Skip to content

Commit

Permalink
StealEmoji: check remote size before downloading
Browse files Browse the repository at this point in the history
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 sneako/finch#224
  • Loading branch information
TheOneric authored and animeavi committed Apr 3, 2024
1 parent 3e154ce commit e716c7e
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 4 deletions.
4 changes: 3 additions & 1 deletion docs/docs/configuration/cheatsheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@ config :pleroma, :mrf_user_allowlist, %{
#### :mrf_steal_emoji
* `hosts`: List of hosts to steal emojis from
* `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

Expand Down
25 changes: 23 additions & 2 deletions lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do

@pack_name "stolen"

# Config defaults
@size_limit 50_000
@download_unknown_size false

defp create_pack() do
with {:ok, pack} = Pack.create(@pack_name) do
Pack.save_metadata(
Expand Down Expand Up @@ -97,11 +101,28 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
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
url = Pleroma.Web.MediaProxy.url(url)

with {:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do
size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
with {:remote_size, true} <- {:remote_size, is_remote_size_within_limit?(url)},
{: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)

if byte_size(response.body) <= size_limit and extension do
Expand Down
50 changes: 49 additions & 1 deletion test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
) do
quote do
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)} ->
%Tesla.Env{
status: unquote(status),
Expand All @@ -46,7 +54,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
setup do
clear_config(:mrf_steal_emoji,
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")
Expand Down Expand Up @@ -174,5 +183,44 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
refute "firedfox" in installed()
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)
end

0 comments on commit e716c7e

Please sign in to comment.