Scrub content-type of uploaded media before serving

This commit is contained in:
Mark Felder 2023-05-27 13:02:27 -04:00
parent 31ec5cd35e
commit 64f4797b21
4 changed files with 85 additions and 0 deletions

1
changelog.d/3895.add Normal file
View file

@ -0,0 +1 @@
Uploaded media content-type is scrubbed before serving the files to limit the security impact of some attachments

View file

@ -51,6 +51,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia 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
_ ->
conn
@ -111,4 +112,26 @@ defmodule Pleroma.Web.Plugs.UploadedMedia 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

32
test/fixtures/snow.js vendored Normal file
View file

@ -0,0 +1,32 @@
function createSnowflake() {
const snowflake = document.createElement('span');
snowflake.innerHTML = '❅';
snowflake.style.position = 'absolute';
snowflake.style.color = '#fff';
snowflake.style.userSelect = 'none';
snowflake.style.pointerEvents = 'none';
snowflake.style.fontSize = Math.random() * 20 + 'px';
snowflake.style.left = Math.random() * window.innerWidth + 'px';
snowflake.style.animation = 'fall ' + (Math.random() * 5 + 5) + 's linear infinite';
return snowflake;
}
function createSnowfall() {
const snowfallContainer = document.createElement('div');
snowfallContainer.style.position = 'fixed';
snowfallContainer.style.top = '0';
snowfallContainer.style.left = '0';
snowfallContainer.style.width = '100%';
snowfallContainer.style.height = '100%';
snowfallContainer.style.pointerEvents = 'none';
snowfallContainer.style.zIndex = '9999';
for (let i = 0; i < 50; i++) {
const snowflake = createSnowflake();
snowfallContainer.appendChild(snowflake);
}
document.body.appendChild(snowfallContainer);
}
window.addEventListener('load', createSnowfall);

View file

@ -40,4 +40,33 @@ defmodule Pleroma.Web.Plugs.UploadedMediaPlugTest do
&(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]})
)
end
test "Filters out dangerous content types" do
context = %{module: __MODULE__, case: __MODULE__}
test_files = [
"test/fixtures/lain.xml",
"test/fixtures/nypd-facial-recognition-children-teenagers.html",
"test/fixtures/snow.js"
]
Enum.each(test_files, fn t ->
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