diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex index ec188986b..5456634a1 100644 --- a/lib/pleroma/web/plugs/instance_static.ex +++ b/lib/pleroma/web/plugs/instance_static.ex @@ -62,7 +62,7 @@ defp call_static(%{request_path: request_path} = conn, opts, from) do opts = opts |> Map.put(:from, from) - |> Map.put(:content_type, false) + |> Map.put(:content_types, false) conn |> set_static_content_type(request_path) diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex index 357fcb432..15d6190df 100644 --- a/lib/pleroma/web/plugs/uploaded_media.ex +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -88,7 +88,7 @@ defp get_media(conn, {:static_dir, directory}, opts) 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/content_type_sanitisation_test.exs b/test/pleroma/web/content_type_sanitisation_test.exs new file mode 100644 index 000000000..6aa335b6e --- /dev/null +++ b/test/pleroma/web/content_type_sanitisation_test.exs @@ -0,0 +1,66 @@ +# Akkoma: Magically expressive social media +# Copyright © 2025 Akkoma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ContentTypeSanitisationTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Web.ContentTypeSanitisationTemplate, as: Template + + require Template + + defp create_file(path, body) do + File.write!(path, body) + on_exit(fn -> File.rm(path) end) + end + + defp upload_dir(), + do: Path.join(Pleroma.Uploaders.Local.upload_path(), "test_StaticPlugSanitisationTest") + + defp create_upload(subpath, body) do + Path.join(upload_dir(), subpath) + |> create_file(body) + + "/media/test_StaticPlugSanitisationTest/#{subpath}" + end + + defp emoji_dir(), + do: + Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji/test_StaticPlugSanitisationTest" + ) + + defp create_emoji(subpath, body) do + Path.join(emoji_dir(), subpath) + |> create_file(body) + + "/emoji/test_StaticPlugSanitisationTest/#{subpath}" + end + + setup_all do + File.mkdir_p(upload_dir()) + File.mkdir_p(emoji_dir()) + + on_exit(fn -> + File.rm_rf!(upload_dir()) + File.rm_rf!(emoji_dir()) + end) + end + + describe "sanitises Content-Type of local uploads" do + Template.do_all_common_tests(&create_upload/2) + + test "while preserving audio types" do + Template.do_audio_test(&create_upload/2, false) + end + end + + describe "sanitises Content-Type of emoji" do + Template.do_all_common_tests(&create_emoji/2) + + test "if audio type" do + Template.do_audio_test(&create_emoji/2, true) + end + end +end diff --git a/test/support/web/content_type_sanitisation_template.ex b/test/support/web/content_type_sanitisation_template.ex new file mode 100644 index 000000000..c785a0e87 --- /dev/null +++ b/test/support/web/content_type_sanitisation_template.ex @@ -0,0 +1,81 @@ +# Akkoma: Magically expressive social media +# Copyright © 2025 Akkoma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ContentTypeSanitisationTemplate do + defmacro do_test(create_fun, fname, body, content_type) do + quote do + url = unquote(create_fun).(unquote(fname), unquote(body)) + resp = get(build_conn(), url) + assert resp.status == 200 + + assert Enum.all?( + Plug.Conn.get_resp_header(resp, "content-type"), + fn e -> e == unquote(content_type) end + ) + end + end + + defmacro do_all_common_tests(create_fun) do + quote do + test "while preserving image types" do + unquote(__MODULE__).do_test( + unquote(create_fun), + "image.jpg", + File.read!("test/fixtures/image.jpg"), + "image/jpeg" + ) + end + + test "if ActivityPub type" do + # this already ought to be impossible from the configured MIME mapping, but let’s make sure + unquote(__MODULE__).do_test( + unquote(create_fun), + "ap.activity+json", + "{\"a\": \"b\"}", + "application/octet-stream" + ) + end + + test "if PDF type" do + unquote(__MODULE__).do_test( + unquote(create_fun), + "pdf.pdf", + "pdf stub", + "application/octet-stream" + ) + end + + test "if Javascript type" do + unquote(__MODULE__).do_test( + unquote(create_fun), + "script.js", + "alert('miaow');", + "application/octet-stream" + ) + end + + test "if CSS type" do + unquote(__MODULE__).do_test( + unquote(create_fun), + "script.js", + ".StatusBody {display: none;}", + "application/octet-stream" + ) + end + end + end + + defmacro do_audio_test(create_fun, sanitise \\ false) do + quote do + expected_type = if unquote(sanitise), do: "application/octet-stream", else: "audio/mpeg" + + unquote(__MODULE__).do_test( + unquote(create_fun), + "audio.mp3", + File.read!("test/fixtures/sound.mp3"), + expected_type + ) + end + end +end