Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85595397
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
7 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/changelog.d/3895.add b/changelog.d/3895.add
new file mode 100644
index 000000000..5b1737ebd
--- /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 8b3bc9acb..38c00c1a3 100644
--- a/lib/pleroma/web/plugs/uploaded_media.ex
+++ b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -1,114 +1,137 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UploadedMedia do
@moduledoc """
"""
import Plug.Conn
import Pleroma.Web.Gettext
require Logger
alias Pleroma.Web.MediaProxy
@behaviour Plug
# no slashes
@path "media"
@default_cache_control_header "public, max-age=1209600"
def init(_opts) do
static_plug_opts =
[
headers: %{"cache-control" => @default_cache_control_header},
cache_control_for_etags: @default_cache_control_header
]
|> Keyword.put(:from, "__unconfigured_media_plug")
|> Keyword.put(:at, "/__unconfigured_media_plug")
|> Plug.Static.init()
%{static_plug_opts: static_plug_opts}
end
def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
conn =
case fetch_query_params(conn) do
%{query_params: %{"name" => name}} = conn ->
name = String.replace(name, ~s["], ~s[\\"])
put_resp_header(conn, "content-disposition", ~s[inline; filename="#{name}"])
conn ->
conn
end
|> merge_resp_headers([{"content-security-policy", "sandbox"}])
config = Pleroma.Config.get(Pleroma.Upload)
with uploader <- Keyword.fetch!(config, :uploader),
proxy_remote = Keyword.get(config, :proxy_remote, false),
{: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
|> send_resp(:internal_server_error, dgettext("errors", "Failed"))
|> halt()
end
end
def call(conn, _opts), do: conn
defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do
MediaProxy.in_banned_urls(Pleroma.Upload.base_url() <> path)
end
defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url)
defp media_is_banned(_, _), do: false
defp get_media(conn, {:static_dir, directory}, _, opts) do
static_opts =
Map.get(opts, :static_plug_opts)
|> Map.put(:at, [@path])
|> Map.put(:from, directory)
conn = Plug.Static.call(conn, static_opts)
if conn.halted do
conn
else
conn
|> send_resp(:not_found, dgettext("errors", "Not found"))
|> halt()
end
end
defp get_media(conn, {:url, url}, true, _) do
proxy_opts = [
http: [
follow_redirect: true,
pool: :upload
]
]
conn
|> Pleroma.ReverseProxy.call(url, proxy_opts)
end
defp get_media(conn, {:url, url}, _, _) do
conn
|> Phoenix.Controller.redirect(external: url)
|> halt()
end
defp get_media(conn, unknown, _, _) do
Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}")
conn
|> 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 000000000..ff53af709
--- /dev/null
+++ b/test/fixtures/snow.js
@@ -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);
diff --git a/test/pleroma/web/plugs/uploaded_media_plug_test.exs b/test/pleroma/web/plugs/uploaded_media_plug_test.exs
index 8323ff6ab..6e9dade21 100644
--- a/test/pleroma/web/plugs/uploaded_media_plug_test.exs
+++ b/test/pleroma/web/plugs/uploaded_media_plug_test.exs
@@ -1,43 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UploadedMediaPlugTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Upload
defp upload_file(context) do
Pleroma.DataCase.ensure_local_uploader(context)
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "nice_tf.jpg"
}
{:ok, data} = Upload.store(file)
[%{"href" => attachment_url} | _] = data["url"]
[attachment_url: attachment_url]
end
setup_all :upload_file
test "does not send Content-Disposition header when name param is not set", %{
attachment_url: attachment_url
} do
conn = get(build_conn(), attachment_url)
refute Enum.any?(conn.resp_headers, &(elem(&1, 0) == "content-disposition"))
end
test "sends Content-Disposition header when name param is set", %{
attachment_url: attachment_url
} do
conn = get(build_conn(), attachment_url <> ~s[?name="cofe".gif])
assert Enum.any?(
conn.resp_headers,
&(&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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Jul 18, 11:06 PM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1695157
Default Alt Text
(7 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment