From a2ee5e4ccf829600cb4a0a5acca924e25991b4ad Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 27 May 2023 13:02:27 -0400 Subject: [PATCH] Scrub content-type of uploaded media before serving --- changelog.d/3895.add | 1 + lib/pleroma/web/plugs/uploaded_media.ex | 23 ++++++++++++++ test/fixtures/snow.js | Bin 0 -> 1113 bytes .../web/plugs/uploaded_media_plug_test.exs | 29 ++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 changelog.d/3895.add create mode 100644 test/fixtures/snow.js diff --git a/changelog.d/3895.add b/changelog.d/3895.add new file mode 100644 index 0000000000..5b1737ebdc --- /dev/null +++ b/changelog.d/3895.add @@ -0,0 +1 @@ +Uploaded media content-type is scrubbed before serving the files to limit the security impact of some attachments diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex index 9dd5eb2398..0d71481635 100644 --- a/lib/pleroma/web/plugs/uploaded_media.ex +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -55,6 +55,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do {:ok, get_method} <- uploader.get_file(file), false <- media_is_banned(conn, get_method) do get_media(conn, get_method, proxy_remote, opts) + |> scrub_mime() else {:valid_host, false} -> redirect_url = @@ -131,4 +132,26 @@ defp get_media(conn, unknown, _, _) do |> send_resp(:internal_server_error, dgettext("errors", "Internal Error")) |> halt() end + + defp scrub_mime(%Plug.Conn{resp_headers: headers} = conn) do + [{_, mimetype}] = Enum.filter(headers, fn {x, _y} -> match?("content-type", x) end) + + [_type, subtype] = String.split(mimetype, "/") + + cond do + String.contains?(subtype, ["javascript", "ecmascript", "jscript"]) -> + force_plaintext(conn) + + String.contains?(mimetype, ["text/html", "text/xml", "application/xml"]) -> + force_plaintext(conn) + + true -> + conn + end + end + + defp force_plaintext(conn) do + conn + |> merge_resp_headers([{"content-type", "text/plain"}]) + end end diff --git a/test/fixtures/snow.js b/test/fixtures/snow.js new file mode 100644 index 0000000000000000000000000000000000000000..ff53af709cc7601129641ec72c416e5a1a17b485 GIT binary patch literal 1113 zcmah}%TB{E5WM#*EFqMXq9`EsfC{Io5=iZ-NSsZ)CM(=^WIKI;IPw|%9ACmZEws?k z<`Tu7nQUfus%t6&S`vi=8DL6wp^bcov@2daLKK#K5dQmA+z4fhdIl7-HDP2RKBbAT zBqiMebAHen5`nwDpNyH8e15zqzkl{&Wgx4p$7TY?$VYEY9iSChSYusOI?-CUB5DtA z3I>W0VPvUYsIo`_+-S~&`-4brDNOMN7&MWg%$y`;XY9?HxXeXQB-OHY5Cb}n@Rzed zrZ3jEJfI5Y0SQSsli$8*X^co1o&J^V^I^Xmb-XZ0Ae}(cNVqZ4L8u*VGZ}Q=Ivw2- zI=^8*b3Poh1(JoM^Z#Y9aK01$E-Bm3SmP4Z*0mONuvJVYKM2}bd<3;0C zR{}nj&E6b^9@(FkFM7En1 + Pleroma.DataCase.ensure_local_uploader(context) + filename = String.split(t, "/") |> List.last() + + upload = %Plug.Upload{ + path: Path.absname(t), + filename: filename + } + + {:ok, %{"url" => [%{"href" => attachment_url}]}} = Upload.store(upload) + + conn = get(build_conn(), attachment_url) + + assert Enum.any?( + conn.resp_headers, + &(&1 == {"content-type", "text/plain"}) + ) + end) + end end