Fix Content-Type sanitisation for emoji and local uploads
Some checks failed
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-arm64 Pipeline failed
ci/woodpecker/push/build-amd64 Pipeline failed
ci/woodpecker/push/docs unknown status

This was accidentally broken in c8e0f7848b
due to a one-letter mistake in the plug option name and an absence of
tests. Therefore it was once again possible to serve e.g. Javascript or
CSS payloads via uploads and emoji.
However due to other protections it was still NOT possible for anyone to
serve any payload with an ActivityPub Content-Type. With the CSP policy
hardening from previous JS payload exloits predating the Content-Type
sanitisation, there is currently no known way of abusing this weakened
Content-Type sanitisation, but should be fixed regardless.

This commit fixes the option name and adds tests to ensure
such a regression doesn't occur again in the future.

Reported-by: Lain Soykaf <lain@lain.com>
This commit is contained in:
Oneric 2025-03-10 18:45:12 +01:00
parent fc2c740008
commit 066d5b48ed
4 changed files with 149 additions and 2 deletions

View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,66 @@
# Akkoma: Magically expressive social media
# Copyright © 2025 Akkoma Authors <https://akkoma.dev/>
# 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

View file

@ -0,0 +1,81 @@
# Akkoma: Magically expressive social media
# Copyright © 2025 Akkoma Authors <https://akkoma.dev/>
# 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 lets 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