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
|
||||
* `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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
@ -97,11 +101,28 @@ defp get_extension_if_safe(response) 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
|
||||
|
|
|
@ -32,6 +32,14 @@ defmacro mock_tesla(
|
|||
) 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),
|
||||
|
@ -46,7 +54,8 @@ defmacro mock_tesla(
|
|||
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")
|
||||
|
@ -174,5 +183,44 @@ test "reject if host returns error", %{message: message} 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
|
||||
|
|
Loading…
Reference in a new issue