Sanitize media uploads.

This commit is contained in:
Lain Soykaf 2025-03-10 17:23:21 +04:00
parent b469b9d9d3
commit 1dd9ba5d6f
3 changed files with 113 additions and 9 deletions

View file

@ -83,7 +83,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
Map.get(opts, :static_plug_opts)
|> Map.put(:at, [@path])
|> Map.put(:from, directory)
|> Map.put(:content_type, false)
|> Map.put(:content_types, false)
conn =
conn

View file

@ -227,4 +227,93 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do
|> json_response_and_validate_schema(403)
end
end
describe "Content-Type sanitization" do
setup do: oauth_access(["write:media", "read:media"])
setup do
ConfigMock
|> stub_with(Pleroma.Test.StaticConfig)
config =
Pleroma.Config.get([Pleroma.Upload])
|> Keyword.put(:uploader, Pleroma.Uploaders.Local)
clear_config([Pleroma.Upload], config)
clear_config([Pleroma.Upload, :allowed_mime_types], ["image", "audio", "video"])
# Create a file with a malicious content type and dangerous extension
malicious_file = %Plug.Upload{
content_type: "application/activity+json",
path: Path.absname("test/fixtures/image.jpg"),
# JSON extension to make MIME.from_path detect application/json
filename: "malicious.json"
}
[malicious_file: malicious_file]
end
test "sanitizes malicious content types when serving media", %{
conn: conn,
malicious_file: malicious_file
} do
# First upload the file with the malicious content type
media =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v1/media", %{"file" => malicious_file})
|> json_response_and_validate_schema(:ok)
# Get the file URL from the response
url = media["url"]
# Now make a direct request to the media URL and check the content-type header
response =
build_conn()
|> get(URI.parse(url).path)
# Find the content-type header
content_type_header =
Enum.find(response.resp_headers, fn {name, _} -> name == "content-type" end)
# The server should detect the application/json MIME type from the .json extension
# and replace it with application/octet-stream since it's not in allowed_mime_types
assert content_type_header == {"content-type", "application/octet-stream"}
# Verify that the file was still served correctly
assert response.status == 200
end
test "allows safe content types", %{conn: conn} do
safe_image = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "safe_image.jpg"
}
# Upload a file with a safe content type
media =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v1/media", %{"file" => safe_image})
|> json_response_and_validate_schema(:ok)
# Get the file URL from the response
url = media["url"]
# Make a direct request to the media URL and check the content-type header
response =
build_conn()
|> get(URI.parse(url).path)
# The server should preserve the image/jpeg MIME type since it's allowed
content_type_header =
Enum.find(response.resp_headers, fn {name, _} -> name == "content-type" end)
assert content_type_header == {"content-type", "image/jpeg"}
# Verify that the file was served correctly
assert response.status == 200
end
end
end

View file

@ -3,17 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UploadedMediaTest do
use Pleroma.Web.ConnCase, async: false
use ExUnit.Case, async: true
alias Pleroma.StaticStubbedConfigMock
alias Pleroma.Web.Plugs.Utils
setup do
Mox.stub_with(StaticStubbedConfigMock, Pleroma.Test.StaticConfig)
{:ok, %{}}
end
describe "content-type sanitization with Utils.get_safe_mime_type/2" do
test "it allows safe MIME types" do
opts = %{allowed_mime_types: ["image", "audio", "video"]}
@ -34,5 +27,27 @@ defmodule Pleroma.Web.Plugs.UploadedMediaTest do
assert Utils.get_safe_mime_type(opts, "application/javascript") ==
"application/octet-stream"
end
test "it sanitizes ActivityPub content types" do
opts = %{allowed_mime_types: ["image", "audio", "video"]}
assert Utils.get_safe_mime_type(opts, "application/activity+json") ==
"application/octet-stream"
assert Utils.get_safe_mime_type(opts, "application/ld+json") == "application/octet-stream"
assert Utils.get_safe_mime_type(opts, "application/jrd+json") == "application/octet-stream"
end
test "it sanitizes other potentially dangerous types" do
opts = %{allowed_mime_types: ["image", "audio", "video"]}
assert Utils.get_safe_mime_type(opts, "text/html") == "application/octet-stream"
assert Utils.get_safe_mime_type(opts, "application/javascript") ==
"application/octet-stream"
assert Utils.get_safe_mime_type(opts, "text/javascript") == "application/octet-stream"
assert Utils.get_safe_mime_type(opts, "application/xhtml+xml") == "application/octet-stream"
end
end
end