StealEmoji: use Content-Type and reject non-images

E.g. *key’s emoji URLs typically don’t have file extensions, but
until now we just slapped ".png" at its end hoping for the best.

Furthermore, this gives us a chance to actually reject non-images,
which before was not feasible exatly due to those extension-less URLs
This commit is contained in:
Oneric 2024-03-07 23:35:05 +01:00
parent 111cdb0d86
commit a8c6c780b4
2 changed files with 62 additions and 15 deletions

View file

@ -33,16 +33,9 @@ defp reject_emoji?({shortcode, _url}, installed_emoji) do
!valid_shortcode? or rejected_shortcode? or emoji_installed?
end
defp steal_emoji(%{} = response, {shortcode, url}, emoji_dir_path) do
extension =
url
|> URI.parse()
|> Map.get(:path)
|> Path.basename()
|> Path.extname()
defp steal_emoji(%{} = response, {shortcode, extension}, emoji_dir_path) do
shortcode = Path.basename(shortcode)
file_path = Path.join(emoji_dir_path, shortcode <> (extension || ".png"))
file_path = Path.join(emoji_dir_path, shortcode <> "." <> extension)
case File.write(file_path, response.body) do
:ok ->
@ -54,14 +47,25 @@ defp steal_emoji(%{} = response, {shortcode, url}, emoji_dir_path) do
end
end
defp get_extension_if_safe(response) do
content_type =
:proplists.get_value("content-type", response.headers, MIME.from_path(response.url))
case content_type do
"image/" <> _ -> List.first(MIME.extensions(content_type))
_ -> nil
end
end
defp maybe_steal_emoji({shortcode, url}, emoji_dir_path) 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)
extension = get_extension_if_safe(response)
if byte_size(response.body) <= size_limit do
steal_emoji(response, {shortcode, url}, emoji_dir_path)
if byte_size(response.body) <= size_limit and extension do
steal_emoji(response, {shortcode, extension}, emoji_dir_path)
else
Logger.debug(
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{size_limit} B)"

View file

@ -45,7 +45,11 @@ test "Steals emoji on unknown shortcode from allowed remote host", %{
refute File.exists?(path)
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox.png"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/image.jpg"),
url: "https://example.org/emoji/firedfox.png"
}
end)
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
@ -72,7 +76,11 @@ test "rejects invalid shortcodes", %{path: path} do
fullpath = Path.join(path, "fired/fox.png")
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/image.jpg"),
url: "https://example.org/emoji/firedfox.png"
}
end)
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
@ -86,6 +94,36 @@ test "rejects invalid shortcodes", %{path: path} do
refute File.exists?(fullpath)
end
test "prefers content-type header for extension", %{path: path} do
message = %{
"type" => "Create",
"object" => %{
"emoji" => [{"firedfox", "https://example.org/emoji/firedfox.fud"}],
"actor" => "https://example.org/users/admin"
}
}
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox.fud"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/image.jpg"),
url: "https://example.org/emoji/firedfox.wevp",
headers: [{"content-type", "image/gif"}]
}
end)
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
assert {:ok, _message} = StealEmojiPolicy.filter(message)
assert "firedfox" in installed()
assert File.exists?(path)
assert path
|> Path.join("firedfox.gif")
|> File.exists?()
end
test "reject regex shortcode", %{message: message} do
refute "firedfox" in installed()
@ -118,7 +156,11 @@ test "reject if size is above the limit", %{message: message} do
refute "firedfox" in installed()
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox.png"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/image.jpg"),
url: "https://example.org/emoji/firedfox.png"
}
end)
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 50_000)
@ -132,7 +174,8 @@ test "reject if host returns error", %{message: message} do
refute "firedfox" in installed()
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox.png"} ->
{:ok, %Tesla.Env{status: 404, body: "Not found"}}
{:ok,
%Tesla.Env{status: 404, body: "Not found", url: "https://example.org/emoji/firedfox.png"}}
end)
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)