StealEmoji: check remote size before downloading
To save on bandwith and avoid OOMs with large files. Ofc, this relies on the remote server (a) sending a content-length header and (b) being honest about the size. Common fedi servers seem to provide the header and (b) at least raises the required privilege of an malicious actor to a server infrastructure admin of an explicitly allowed host. A more complete defense which still works when faced with a malicious server requires changes in upstream Finch; see https://github.com/sneako/finch/issues/224
This commit is contained in:
parent
6d003e1acd
commit
d6d838cbe8
3 changed files with 75 additions and 4 deletions
|
@ -236,7 +236,9 @@ config :pleroma, :mrf_user_allowlist, %{
|
||||||
#### :mrf_steal_emoji
|
#### :mrf_steal_emoji
|
||||||
* `hosts`: List of hosts to steal emojis from
|
* `hosts`: List of hosts to steal emojis from
|
||||||
* `rejected_shortcodes`: Regex-list of shortcodes to reject
|
* `rejected_shortcodes`: Regex-list of shortcodes to reject
|
||||||
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
|
* `size_limit`: File size limit (in bytes), checked before download if possible (and remote server honest),
|
||||||
|
otherwise or again checked before saving emoji to the disk
|
||||||
|
* `download_unknown_size`: whether to download an emoji when the remote server doesn’t report its size in advance
|
||||||
|
|
||||||
#### :mrf_activity_expiration
|
#### :mrf_activity_expiration
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
||||||
|
|
||||||
@pack_name "stolen"
|
@pack_name "stolen"
|
||||||
|
|
||||||
|
# Config defaults
|
||||||
|
@size_limit 50_000
|
||||||
|
@download_unknown_size false
|
||||||
|
|
||||||
defp create_pack() do
|
defp create_pack() do
|
||||||
with {:ok, pack} = Pack.create(@pack_name) do
|
with {:ok, pack} = Pack.create(@pack_name) do
|
||||||
Pack.save_metadata(
|
Pack.save_metadata(
|
||||||
|
@ -97,11 +101,28 @@ defp get_extension_if_safe(response) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp is_remote_size_within_limit?(url) do
|
||||||
|
with {:ok, %{status: status, headers: headers} = _response} when status in 200..299 <-
|
||||||
|
Pleroma.HTTP.request(:head, url, nil, [], []) do
|
||||||
|
content_length = :proplists.get_value("content-length", headers, nil)
|
||||||
|
size_limit = Config.get([:mrf_steal_emoji, :size_limit], @size_limit)
|
||||||
|
|
||||||
|
accept_unknown =
|
||||||
|
Config.get([:mrf_steal_emoji, :download_unknown_size], @download_unknown_size)
|
||||||
|
|
||||||
|
content_length <= size_limit or
|
||||||
|
(content_length == nil and accept_unknown)
|
||||||
|
else
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp maybe_steal_emoji({shortcode, url}) do
|
defp maybe_steal_emoji({shortcode, url}) do
|
||||||
url = Pleroma.Web.MediaProxy.url(url)
|
url = Pleroma.Web.MediaProxy.url(url)
|
||||||
|
|
||||||
with {:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do
|
with {:remote_size, true} <- {:remote_size, is_remote_size_within_limit?(url)},
|
||||||
size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
|
{:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do
|
||||||
|
size_limit = Config.get([:mrf_steal_emoji, :size_limit], @size_limit)
|
||||||
extension = get_extension_if_safe(response)
|
extension = get_extension_if_safe(response)
|
||||||
|
|
||||||
if byte_size(response.body) <= size_limit and extension do
|
if byte_size(response.body) <= size_limit and extension do
|
||||||
|
|
|
@ -32,6 +32,14 @@ defmacro mock_tesla(
|
||||||
) do
|
) do
|
||||||
quote do
|
quote do
|
||||||
Tesla.Mock.mock(fn
|
Tesla.Mock.mock(fn
|
||||||
|
%{method: :head, url: unquote(url)} ->
|
||||||
|
%Tesla.Env{
|
||||||
|
status: unquote(status),
|
||||||
|
body: nil,
|
||||||
|
url: unquote(url),
|
||||||
|
headers: unquote(headers)
|
||||||
|
}
|
||||||
|
|
||||||
%{method: :get, url: unquote(url)} ->
|
%{method: :get, url: unquote(url)} ->
|
||||||
%Tesla.Env{
|
%Tesla.Env{
|
||||||
status: unquote(status),
|
status: unquote(status),
|
||||||
|
@ -46,7 +54,8 @@ defmacro mock_tesla(
|
||||||
setup do
|
setup do
|
||||||
clear_config(:mrf_steal_emoji,
|
clear_config(:mrf_steal_emoji,
|
||||||
hosts: ["example.org"],
|
hosts: ["example.org"],
|
||||||
size_limit: 284_468
|
size_limit: 284_468,
|
||||||
|
download_unknown_size: true
|
||||||
)
|
)
|
||||||
|
|
||||||
emoji_path = [:instance, :static_dir] |> Config.get() |> Path.join("emoji/stolen")
|
emoji_path = [:instance, :static_dir] |> Config.get() |> Path.join("emoji/stolen")
|
||||||
|
@ -174,5 +183,44 @@ test "reject if host returns error", %{message: message} do
|
||||||
refute "firedfox" in installed()
|
refute "firedfox" in installed()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "reject unknown size", %{message: message} do
|
||||||
|
clear_config([:mrf_steal_emoji, :download_unknown_size], false)
|
||||||
|
mock_tesla()
|
||||||
|
|
||||||
|
refute "firedfox" in installed()
|
||||||
|
|
||||||
|
ExUnit.CaptureLog.capture_log(fn ->
|
||||||
|
assert {:ok, _message} = StealEmojiPolicy.filter(message)
|
||||||
|
end) =~
|
||||||
|
"MRF.StealEmojiPolicy: Failed to fetch https://example.org/emoji/firedfox.png: {:remote_size, false}"
|
||||||
|
|
||||||
|
refute "firedfox" in installed()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reject too large content-size before download", %{message: message} do
|
||||||
|
clear_config([:mrf_steal_emoji, :download_unknown_size], false)
|
||||||
|
mock_tesla("https://example.org/emoji/firedfox.png", 200, [{"content-length", 2 ** 30}])
|
||||||
|
|
||||||
|
refute "firedfox" in installed()
|
||||||
|
|
||||||
|
ExUnit.CaptureLog.capture_log(fn ->
|
||||||
|
assert {:ok, _message} = StealEmojiPolicy.filter(message)
|
||||||
|
end) =~
|
||||||
|
"MRF.StealEmojiPolicy: Failed to fetch https://example.org/emoji/firedfox.png: {:remote_size, false}"
|
||||||
|
|
||||||
|
refute "firedfox" in installed()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "accepts content-size below limit", %{message: message} do
|
||||||
|
clear_config([:mrf_steal_emoji, :download_unknown_size], false)
|
||||||
|
mock_tesla("https://example.org/emoji/firedfox.png", 200, [{"content-length", 2}])
|
||||||
|
|
||||||
|
refute "firedfox" in installed()
|
||||||
|
|
||||||
|
assert {:ok, _message} = StealEmojiPolicy.filter(message)
|
||||||
|
|
||||||
|
assert "firedfox" in installed()
|
||||||
|
end
|
||||||
|
|
||||||
defp installed, do: Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
|
defp installed, do: Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue