commit: c14365336411f43f0e9eea00bc1c8242620220f1
parent d9ae9b676c2963466cbb8e440711db1759e25c31
Author: Lain Soykaf <lain@lain.com>
Date: Tue, 11 Mar 2025 14:18:36 +0400
ReverseProxy: Sanitize content.
Diffstat:
2 files changed, 90 insertions(+), 5 deletions(-)
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
@@ -17,6 +17,8 @@ defmodule Pleroma.ReverseProxy do
@failed_request_ttl :timer.seconds(60)
@methods ~w(GET HEAD)
+ @allowed_mime_types Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types], [])
+
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def max_read_duration_default, do: @max_read_duration
@@ -301,10 +303,26 @@ defmodule Pleroma.ReverseProxy do
headers
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|> build_resp_cache_headers(opts)
+ |> sanitise_content_type()
|> build_resp_content_disposition_header(opts)
|> Keyword.merge(Keyword.get(opts, :resp_headers, []))
end
+ defp sanitise_content_type(headers) do
+ original_ct = get_content_type(headers)
+
+ safe_ct =
+ Pleroma.Web.Plugs.Utils.get_safe_mime_type(
+ %{allowed_mime_types: @allowed_mime_types},
+ original_ct
+ )
+
+ [
+ {"content-type", safe_ct}
+ | Enum.filter(headers, fn {k, _v} -> k != "content-type" end)
+ ]
+ end
+
defp build_resp_cache_headers(headers, _opts) do
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs
@@ -63,7 +63,11 @@ defmodule Pleroma.ReverseProxyTest do
|> Plug.Conn.put_req_header("user-agent", "fake/1.0")
|> ReverseProxy.call("/user-agent")
- assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()}
+ # Convert the response to a map without relying on json_response
+ body = conn.resp_body
+ assert conn.status == 200
+ response = Jason.decode!(body)
+ assert response == %{"user-agent" => Pleroma.Application.user_agent()}
end
test "closed connection", %{conn: conn} do
@@ -138,11 +142,14 @@ defmodule Pleroma.ReverseProxyTest do
test "common", %{conn: conn} do
ClientMock
|> expect(:request, fn :head, "/head", _, _, _ ->
- {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]}
+ {:ok, 200, [{"content-type", "image/png"}]}
end)
conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head")
- assert html_response(conn, 200) == ""
+
+ assert conn.status == 200
+ assert Conn.get_resp_header(conn, "content-type") == ["image/png"]
+ assert conn.resp_body == ""
end
end
@@ -249,7 +256,10 @@ defmodule Pleroma.ReverseProxyTest do
)
|> ReverseProxy.call("/headers")
- %{"headers" => headers} = json_response(conn, 200)
+ body = conn.resp_body
+ assert conn.status == 200
+ response = Jason.decode!(body)
+ headers = response["headers"]
assert headers["Accept"] == "text/html"
end
@@ -262,7 +272,10 @@ defmodule Pleroma.ReverseProxyTest do
)
|> ReverseProxy.call("/headers")
- %{"headers" => headers} = json_response(conn, 200)
+ body = conn.resp_body
+ assert conn.status == 200
+ response = Jason.decode!(body)
+ headers = response["headers"]
refute headers["Accept-Language"]
end
end
@@ -328,4 +341,58 @@ defmodule Pleroma.ReverseProxyTest do
assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers
end
end
+
+ describe "content-type sanitisation" do
+ test "preserves allowed image type", %{conn: conn} do
+ ClientMock
+ |> expect(:request, fn :get, "/content", _, _, _ ->
+ {:ok, 200, [{"content-type", "image/png"}], %{url: "/content"}}
+ end)
+ |> expect(:stream_body, fn _ -> :done end)
+
+ conn = ReverseProxy.call(conn, "/content")
+
+ assert conn.status == 200
+ assert Conn.get_resp_header(conn, "content-type") == ["image/png"]
+ end
+
+ test "preserves allowed video type", %{conn: conn} do
+ ClientMock
+ |> expect(:request, fn :get, "/content", _, _, _ ->
+ {:ok, 200, [{"content-type", "video/mp4"}], %{url: "/content"}}
+ end)
+ |> expect(:stream_body, fn _ -> :done end)
+
+ conn = ReverseProxy.call(conn, "/content")
+
+ assert conn.status == 200
+ assert Conn.get_resp_header(conn, "content-type") == ["video/mp4"]
+ end
+
+ test "sanitizes ActivityPub content type", %{conn: conn} do
+ ClientMock
+ |> expect(:request, fn :get, "/content", _, _, _ ->
+ {:ok, 200, [{"content-type", "application/activity+json"}], %{url: "/content"}}
+ end)
+ |> expect(:stream_body, fn _ -> :done end)
+
+ conn = ReverseProxy.call(conn, "/content")
+
+ assert conn.status == 200
+ assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
+ end
+
+ test "sanitizes LD-JSON content type", %{conn: conn} do
+ ClientMock
+ |> expect(:request, fn :get, "/content", _, _, _ ->
+ {:ok, 200, [{"content-type", "application/ld+json"}], %{url: "/content"}}
+ end)
+ |> expect(:stream_body, fn _ -> :done end)
+
+ conn = ReverseProxy.call(conn, "/content")
+
+ assert conn.status == 200
+ assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
+ end
+ end
end