commit: b1309bdb403fdbfdb0a8b076a5a13af811191ca9
parent 1dd9ba5d6fa45a8965703c96e9823ac7e41c52be
Author: Lain Soykaf <lain@lain.com>
Date: Mon, 10 Mar 2025 18:44:17 +0400
More fixes for InstanceStatic
Diffstat:
3 files changed, 66 insertions(+), 1 deletion(-)
diff --git a/config/config.exs b/config/config.exs
@@ -66,7 +66,7 @@ config :pleroma, Pleroma.Upload,
filename_display_max_length: 30,
default_description: nil,
base_url: nil,
- allowed_mime_types: ["image", "audio", "video"]
+ allowed_mime_types: ["image", "audio", "video", "text"]
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.Plugs.InstanceStatic do
require Pleroma.Constants
+ import Plug.Conn, only: [put_resp_header: 3]
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration.
@@ -44,10 +45,31 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
end
defp call_static(conn, opts, from) do
+ # Prevent content-type spoofing by setting content_types: false
opts =
opts
|> Map.put(:from, from)
+ |> Map.put(:content_types, false)
+ # Get sanitized content type before calling Plug.Static
+ # Include "text" to allow HTML files and other text-based content
+ allowed_mime_types =
+ Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types], [
+ "image",
+ "audio",
+ "video",
+ "text"
+ ])
+
+ conn = set_content_type(conn, %{allowed_mime_types: allowed_mime_types}, conn.request_path)
+
+ # Call Plug.Static with our sanitized content-type
Plug.Static.call(conn, opts)
end
+
+ defp set_content_type(conn, opts, filepath) do
+ real_mime = MIME.from_path(filepath)
+ clean_mime = Pleroma.Web.Plugs.Utils.get_safe_mime_type(opts, real_mime)
+ put_resp_header(conn, "content-type", clean_mime)
+ end
end
diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs
@@ -62,4 +62,47 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
index = get(build_conn(), "/static/kaniini.html")
assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>"
end
+
+ test "sanitizes content-types for potentially dangerous file extensions" do
+ # Create a file with a potentially dangerous extension (.json)
+ # This mimics an attacker trying to serve ActivityPub JSON with a static file
+ File.mkdir!(@dir <> "/static")
+ File.write!(@dir <> "/static/malicious.json", "{\"type\": \"ActivityPub\"}")
+
+ # Request the malicious file
+ conn = get(build_conn(), "/static/malicious.json")
+
+ # Verify the file was served (status 200)
+ assert conn.status == 200
+
+ # The content should be served, but with a sanitized content-type
+ content_type =
+ Enum.find_value(conn.resp_headers, fn
+ {"content-type", value} -> value
+ _ -> nil
+ end)
+
+ # It should have been sanitized to application/octet-stream because "application"
+ # is not in the allowed_mime_types list
+ assert content_type == "application/octet-stream"
+
+ # Create a file with an allowed extension (.jpg)
+ File.write!(@dir <> "/static/safe.jpg", "fake image data")
+
+ # Request the safe file
+ conn = get(build_conn(), "/static/safe.jpg")
+
+ # Verify the file was served (status 200)
+ assert conn.status == 200
+
+ # Get the content-type
+ content_type =
+ Enum.find_value(conn.resp_headers, fn
+ {"content-type", value} -> value
+ _ -> nil
+ end)
+
+ # It should be preserved because "image" is in the allowed_mime_types list
+ assert content_type == "image/jpeg"
+ end
end