commit: 1dd9ba5d6fa45a8965703c96e9823ac7e41c52be
parent b469b9d9d358a30642d1221a01125af9b6399ff4
Author: Lain Soykaf <lain@lain.com>
Date: Mon, 10 Mar 2025 17:23:21 +0400
Sanitize media uploads.
Diffstat:
3 files changed, 113 insertions(+), 9 deletions(-)
diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -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
diff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs
@@ -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
diff --git a/test/pleroma/web/plugs/uploaded_media_test.exs b/test/pleroma/web/plugs/uploaded_media_test.exs
@@ -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