diff --git a/changelog.d/3831.skip b/changelog.d/3831.skip
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/changelog.d/3896.add b/changelog.d/3896.add
new file mode 100644
index 0000000000..36d8286ff0
--- /dev/null
+++ b/changelog.d/3896.add
@@ -0,0 +1 @@
+Validate Host header for MediaProxy and Uploads and return a 302 if the base_url has changed
diff --git a/changelog.d/3897.add b/changelog.d/3897.add
new file mode 100644
index 0000000000..5c4402f451
--- /dev/null
+++ b/changelog.d/3897.add
@@ -0,0 +1 @@
+OnlyMedia Upload Filter
diff --git a/changelog.d/3899.skip b/changelog.d/3899.skip
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 5c7ff11373..2dbe7f9f64 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -672,6 +672,12 @@ This filter reads the ImageDescription and iptc:Caption-Abstract fields with Exi
No specific configuration.
+#### Pleroma.Upload.Filter.OnlyMedia
+
+This filter rejects uploads that are not identified with Content-Type matching audio/\*, image/\*, or video/\*
+
+No specific configuration.
+
#### Pleroma.Upload.Filter.Mogrify
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`.
diff --git a/lib/pleroma/upload/filter.ex b/lib/pleroma/upload/filter.ex
index 717f066211..809bc6e702 100644
--- a/lib/pleroma/upload/filter.ex
+++ b/lib/pleroma/upload/filter.ex
@@ -38,9 +38,9 @@ def filter([filter | rest], upload) do
{:ok, :noop} ->
filter(rest, upload)
- error ->
- Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(error)}")
- error
+ {:error, e} ->
+ Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(e)}")
+ {:error, e}
end
end
end
diff --git a/lib/pleroma/upload/filter/only_media.ex b/lib/pleroma/upload/filter/only_media.ex
new file mode 100644
index 0000000000..a9caeba67e
--- /dev/null
+++ b/lib/pleroma/upload/filter/only_media.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.OnlyMedia do
+ @behaviour Pleroma.Upload.Filter
+ alias Pleroma.Upload
+
+ def filter(%Upload{content_type: content_type}) do
+ [type, _subtype] = String.split(content_type, "/")
+
+ if type in ["image", "video", "audio"] do
+ {:ok, :noop}
+ else
+ {:error, "Disallowed content-type: #{content_type}"}
+ end
+ end
+
+ def filter(_), do: {:ok, :noop}
+end
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index bda5b36edc..20f3a34389 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -12,6 +12,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
alias Pleroma.Web.MediaProxy
alias Plug.Conn
+ plug(:validate_host)
plug(:sandbox)
def remote(conn, %{"sig" => sig64, "url" => url64}) do
@@ -205,6 +206,30 @@ defp media_proxy_opts do
Config.get([:media_proxy, :proxy_opts], [])
end
+ defp validate_host(conn, _params) do
+ %{scheme: proxy_scheme, host: proxy_host, port: proxy_port} =
+ MediaProxy.base_url() |> URI.parse()
+
+ if match?(^proxy_host, conn.host) do
+ conn
+ else
+ redirect_url =
+ %URI{
+ scheme: proxy_scheme,
+ host: proxy_host,
+ port: proxy_port,
+ path: conn.request_path,
+ query: conn.query_string
+ }
+ |> URI.to_string()
+ |> String.trim_trailing("?")
+
+ conn
+ |> Phoenix.Controller.redirect(external: redirect_url)
+ |> halt()
+ end
+ end
+
defp sandbox(conn, _params) do
conn
|> merge_resp_headers([{"content-security-policy", "sandbox;"}])
diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
index 8b3bc9acbd..9dd5eb2398 100644
--- a/lib/pleroma/web/plugs/uploaded_media.ex
+++ b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -46,12 +46,32 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
config = Pleroma.Config.get(Pleroma.Upload)
- with uploader <- Keyword.fetch!(config, :uploader),
+ %{scheme: media_scheme, host: media_host, port: media_port} =
+ Pleroma.Upload.base_url() |> URI.parse()
+
+ with {:valid_host, true} <- {:valid_host, match?(^media_host, conn.host)},
+ 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)
else
+ {:valid_host, false} ->
+ redirect_url =
+ %URI{
+ scheme: media_scheme,
+ host: media_host,
+ port: media_port,
+ path: conn.request_path,
+ query: conn.query_string
+ }
+ |> URI.to_string()
+ |> String.trim_trailing("?")
+
+ conn
+ |> Phoenix.Controller.redirect(external: redirect_url)
+ |> halt()
+
_ ->
conn
|> send_resp(:internal_server_error, dgettext("errors", "Failed"))
diff --git a/mix.exs b/mix.exs
index d7f8faa107..dd3dcdc65e 100644
--- a/mix.exs
+++ b/mix.exs
@@ -11,7 +11,7 @@ def project do
version: version("2.5.52"),
elixir: "~> 1.11",
elixirc_paths: elixirc_paths(Mix.env()),
- compilers: [:phoenix, :gettext] ++ Mix.compilers(),
+ compilers: [:phoenix] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: warnings_as_errors()],
xref: [exclude: [:eldap]],
start_permanent: Mix.env() == :prod,
@@ -132,10 +132,7 @@ defp deps do
{:telemetry_poller, "~> 1.0"},
# oban 2.14 requires Elixir 1.12+
{:oban, "~> 2.13.4"},
- {:gettext,
- git: "https://github.com/tusooa/gettext.git",
- ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808",
- override: true},
+ {:gettext, "~> 0.20"},
{:bcrypt_elixir, "~> 2.2"},
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.0"},
diff --git a/mix.lock b/mix.lock
index 797207e4e4..d4a0d0c2f7 100644
--- a/mix.lock
+++ b/mix.lock
@@ -44,6 +44,7 @@
"ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
"ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
+ "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
"fast_html": {:hex, :fast_html, "2.0.5", "c61760340606c1077ff1f196f17834056cb1dd3d5cb92a9f2cabf28bc6221c3c", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "605f4f4829443c14127694ebabb681778712ceecb4470ec32aa31012330e6506"},
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
@@ -55,7 +56,7 @@
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
"geo": {:hex, :geo, "3.4.3", "0ddf3f681993d32c397e5ef346e7b4b6f36f39ed138502429832fa4000ebb9d5", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "e23f2892e5437ec8b063cee1beccec89c58fd841ae11133304700235feb25552"},
"geospatial": {:hex, :geospatial, "0.2.0", "c6c9f57df647cabbda71825bbba8465645002922a0c2e6410dc50279dbc95265", [:mix], [{:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:tesla, "~> 1.4.0", [hex: :tesla, repo: "hexpm", optional: false]}, {:tz_world, "~> 1.0", [hex: :tz_world, repo: "hexpm", optional: false]}], "hexpm", "b2f0e8f05a3d40f5473bf546d6b971bb82357e28c4f62c93c160d9e3c3581cb0"},
- "gettext": {:git, "https://github.com/tusooa/gettext.git", "72fb2496b6c5280ed911bdc3756890e7f38a4808", [ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808"]},
+ "gettext": {:hex, :gettext, "0.22.2", "6bfca374de34ecc913a28ba391ca184d88d77810a3e427afa8454a71a51341ac", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "8a2d389673aea82d7eae387e6a2ccc12660610080ae7beb19452cfdc1ec30f60"},
"glob": {:hex, :glob, "1.0.0", "b4d54d66e7797ce037cdd18f2587fc9932187355340e222cafe125cd333d7a0a", [:rebar3], [], "hexpm", "ca25de25ac5a762ba6c979718ae6afef8402cfc9155b87479d215fbe676801e1"},
"gun": {:hex, :gun, "2.0.0", "2326bc0fd6d9cf628419708270d6fe8b02b8d002cf992e4165a77d997b1defd0", [:make, :rebar3], [{:cowlib, "2.12.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "6613cb7c62930dc8d58263c44dda72f8556346ba88358fc929dcbc5f76d04569"},
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
diff --git a/priv/gettext/en_test/LC_MESSAGES/default.po b/priv/gettext/en_test/LC_MESSAGES/default.po
index 63db74608a..037e144662 100644
--- a/priv/gettext/en_test/LC_MESSAGES/default.po
+++ b/priv/gettext/en_test/LC_MESSAGES/default.po
@@ -9,7 +9,6 @@
msgid ""
msgstr ""
"Language: en_test\n"
-"Plural-Forms: nplurals=2\n"
#, elixir-format
#: lib/pleroma/web/api_spec/render_error.ex:122
diff --git a/priv/gettext/en_test/LC_MESSAGES/errors.po b/priv/gettext/en_test/LC_MESSAGES/errors.po
index a40de7f8ba..286bbb1aa8 100644
--- a/priv/gettext/en_test/LC_MESSAGES/errors.po
+++ b/priv/gettext/en_test/LC_MESSAGES/errors.po
@@ -9,7 +9,6 @@
msgid ""
msgstr ""
"Language: en_test\n"
-"Plural-Forms: nplurals=2\n"
msgid "can't be blank"
msgstr ""
diff --git a/priv/gettext/en_test/LC_MESSAGES/posix_errors.po b/priv/gettext/en_test/LC_MESSAGES/posix_errors.po
index 663fc59242..6ff9dc53de 100644
--- a/priv/gettext/en_test/LC_MESSAGES/posix_errors.po
+++ b/priv/gettext/en_test/LC_MESSAGES/posix_errors.po
@@ -9,7 +9,6 @@
msgid ""
msgstr ""
"Language: en_test\n"
-"Plural-Forms: nplurals=2\n"
msgid "eperm"
msgstr ""
diff --git a/priv/gettext/en_test/LC_MESSAGES/static_pages.po b/priv/gettext/en_test/LC_MESSAGES/static_pages.po
index 1a3b7b355b..daf3120938 100644
--- a/priv/gettext/en_test/LC_MESSAGES/static_pages.po
+++ b/priv/gettext/en_test/LC_MESSAGES/static_pages.po
@@ -21,10 +21,6 @@ msgstr ""
#~ ##
#~ ## Use "mix gettext.extract --merge" or "mix gettext.merge"
#~ ## to merge POT files into PO files.
-#~ msgid ""
-#~ msgstr ""
-#~ "Language: en_test\n"
-#~ "Plural-Forms: nplurals=2\n"
#, elixir-format
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
diff --git a/priv/gettext/ru/LC_MESSAGES/errors.po b/priv/gettext/ru/LC_MESSAGES/errors.po
index 39f83e8a67..64218da6fb 100644
--- a/priv/gettext/ru/LC_MESSAGES/errors.po
+++ b/priv/gettext/ru/LC_MESSAGES/errors.po
@@ -9,7 +9,6 @@
msgid ""
msgstr ""
"Language: ru\n"
-"Plural-Forms: nplurals=3\n"
msgid "can't be blank"
msgstr "не может быть пустым"
diff --git a/priv/gettext/zh_Hans/LC_MESSAGES/static_pages.po b/priv/gettext/zh_Hans/LC_MESSAGES/static_pages.po
index cbd6feb60d..809b13d476 100644
--- a/priv/gettext/zh_Hans/LC_MESSAGES/static_pages.po
+++ b/priv/gettext/zh_Hans/LC_MESSAGES/static_pages.po
@@ -24,10 +24,6 @@ msgstr ""
##
## Use "mix gettext.extract --merge" or "mix gettext.merge"
## to merge POT files into PO files.
-#~ msgid ""
-#~ msgstr ""
-#~ "Language: zh_Hans\n"
-#~ "Plural-Forms: nplurals=1\n"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
#, elixir-format
diff --git a/test/pleroma/upload/filter/only_media_test.exs b/test/pleroma/upload/filter/only_media_test.exs
new file mode 100644
index 0000000000..75be070a19
--- /dev/null
+++ b/test/pleroma/upload/filter/only_media_test.exs
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.OnlyMediaTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Upload
+ alias Pleroma.Upload.Filter.OnlyMedia
+
+ test "Allows media Content-Type" do
+ ["audio/mpeg", "image/jpeg", "video/mp4"]
+ |> Enum.each(fn type ->
+ upload = %Upload{
+ content_type: type
+ }
+
+ assert {:ok, :noop} = OnlyMedia.filter(upload)
+ end)
+ end
+
+ test "Disallows non-media Content-Type" do
+ ["application/javascript", "application/pdf", "text/html"]
+ |> Enum.each(fn type ->
+ upload = %Upload{
+ content_type: type
+ }
+
+ assert {:error, _} = OnlyMedia.filter(upload)
+ end)
+ end
+end
diff --git a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs
index 9ce092fd8f..deb407709a 100644
--- a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs
+++ b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs
@@ -54,6 +54,35 @@ test "it returns 403 for invalid signature", %{conn: conn, url: url} do
} = get(conn, "/proxy/hhgfh/eeee/fff")
end
+ test "it returns a 302 for invalid host", %{conn: conn} do
+ new_proxy_base = "http://mp.localhost/"
+
+ %{scheme: new_proxy_scheme, host: new_proxy_host, port: new_proxy_port} =
+ URI.parse(new_proxy_base)
+
+ clear_config([:media_proxy, :base_url], new_proxy_base)
+
+ proxy_url =
+ MediaProxy.encode_url("https://pleroma.social/logo.jpeg")
+ |> URI.parse()
+ |> Map.put(:host, "wronghost")
+ |> URI.to_string()
+
+ expected_url =
+ URI.parse(proxy_url)
+ |> Map.put(:host, new_proxy_host)
+ |> Map.put(:port, new_proxy_port)
+ |> Map.put(:scheme, new_proxy_scheme)
+ |> URI.to_string()
+
+ with_mock Pleroma.ReverseProxy,
+ call: fn _conn, _url, _opts -> %Conn{status: :success} end do
+ conn = get(conn, proxy_url)
+
+ assert redirected_to(conn, 302) == expected_url
+ end
+ end
+
test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do
invalid_url = String.replace(url, "test.png", "test-file.png")
response = get(conn, invalid_url)
diff --git a/test/pleroma/web/plugs/uploaded_media_plug_test.exs b/test/pleroma/web/plugs/uploaded_media_plug_test.exs
index 8323ff6aba..dbf8ca5ec4 100644
--- a/test/pleroma/web/plugs/uploaded_media_plug_test.exs
+++ b/test/pleroma/web/plugs/uploaded_media_plug_test.exs
@@ -40,4 +40,30 @@ test "sends Content-Disposition header when name param is set", %{
&(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]})
)
end
+
+ test "denies access to media if wrong Host", %{
+ attachment_url: attachment_url
+ } do
+ conn = get(build_conn(), attachment_url)
+
+ assert conn.status == 200
+
+ new_media_base = "http://media.localhost:8080"
+
+ %{scheme: new_media_scheme, host: new_media_host, port: new_media_port} =
+ URI.parse(new_media_base)
+
+ clear_config([Pleroma.Upload, :base_url], new_media_base)
+
+ conn = get(build_conn(), attachment_url)
+
+ expected_url =
+ URI.parse(attachment_url)
+ |> Map.put(:host, new_media_host)
+ |> Map.put(:port, new_media_port)
+ |> Map.put(:scheme, new_media_scheme)
+ |> URI.to_string()
+
+ assert redirected_to(conn, 302) == expected_url
+ end
end
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index f010fec332..c1cb0295ba 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -120,6 +120,9 @@ defp json_response_and_validate_schema(conn, _status) do
Mox.verify_on_exit!()
- {:ok, conn: Phoenix.ConnTest.build_conn()}
+ {:ok,
+ conn:
+ Phoenix.ConnTest.build_conn()
+ |> Map.put(:host, Pleroma.Web.Endpoint.host())}
end
end