diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index bb4f4def3..f017bf51b 100644 --- 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 @@ -253,6 +255,7 @@ defp build_resp_headers(headers, opts) 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) |> build_csp_headers() |> Keyword.merge(Keyword.get(opts, :resp_headers, [])) @@ -282,6 +285,21 @@ defp build_resp_cache_headers(headers, _opts) do end 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_content_disposition_header(headers, opts) do opt = Keyword.get(opts, :inline_content_types, @inline_content_types) diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs index e3e2a1571..fc6ae42bc 100644 --- a/test/pleroma/reverse_proxy_test.exs +++ b/test/pleroma/reverse_proxy_test.exs @@ -75,13 +75,16 @@ test "common", %{conn: conn} do Tesla.Mock.mock(fn %{method: :head, url: "/head"} -> %Tesla.Env{ status: 200, - headers: [{"content-type", "text/html; charset=utf-8"}], + headers: [{"content-type", "image/png"}], body: "" } 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 @@ -252,4 +255,38 @@ test "with content-disposition header", %{conn: conn} do assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers end end + + describe "content-type sanitisation" do + test "preserves video type", %{conn: conn} do + Tesla.Mock.mock(fn %{method: :get, url: "/content"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "video/mp4"}], + body: "test" + } + end) + + conn = ReverseProxy.call(Map.put(conn, :method, "GET"), "/content") + + assert conn.status == 200 + assert Conn.get_resp_header(conn, "content-type") == ["video/mp4"] + assert conn.resp_body == "test" + end + + test "replaces application type", %{conn: conn} do + Tesla.Mock.mock(fn %{method: :get, url: "/content"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: "test" + } + end) + + conn = ReverseProxy.call(Map.put(conn, :method, "GET"), "/content") + + assert conn.status == 200 + assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] + assert conn.resp_body == "test" + end + end end