forked from AkkomaGang/akkoma
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:
parent
111cdb0d86
commit
a8c6c780b4
2 changed files with 62 additions and 15 deletions
|
@ -33,16 +33,9 @@ defp reject_emoji?({shortcode, _url}, installed_emoji) do
|
||||||
!valid_shortcode? or rejected_shortcode? or emoji_installed?
|
!valid_shortcode? or rejected_shortcode? or emoji_installed?
|
||||||
end
|
end
|
||||||
|
|
||||||
defp steal_emoji(%{} = response, {shortcode, url}, emoji_dir_path) do
|
defp steal_emoji(%{} = response, {shortcode, extension}, emoji_dir_path) do
|
||||||
extension =
|
|
||||||
url
|
|
||||||
|> URI.parse()
|
|
||||||
|> Map.get(:path)
|
|
||||||
|> Path.basename()
|
|
||||||
|> Path.extname()
|
|
||||||
|
|
||||||
shortcode = Path.basename(shortcode)
|
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
|
case File.write(file_path, response.body) do
|
||||||
:ok ->
|
:ok ->
|
||||||
|
@ -54,14 +47,25 @@ defp steal_emoji(%{} = response, {shortcode, url}, emoji_dir_path) do
|
||||||
end
|
end
|
||||||
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
|
defp maybe_steal_emoji({shortcode, url}, emoji_dir_path) 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 {: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)
|
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
|
if byte_size(response.body) <= size_limit and extension do
|
||||||
steal_emoji(response, {shortcode, url}, emoji_dir_path)
|
steal_emoji(response, {shortcode, extension}, emoji_dir_path)
|
||||||
else
|
else
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{size_limit} B)"
|
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{size_limit} B)"
|
||||||
|
|
|
@ -45,7 +45,11 @@ test "Steals emoji on unknown shortcode from allowed remote host", %{
|
||||||
refute File.exists?(path)
|
refute File.exists?(path)
|
||||||
|
|
||||||
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox.png"} ->
|
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)
|
end)
|
||||||
|
|
||||||
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
|
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")
|
fullpath = Path.join(path, "fired/fox.png")
|
||||||
|
|
||||||
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} ->
|
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)
|
end)
|
||||||
|
|
||||||
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
|
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)
|
refute File.exists?(fullpath)
|
||||||
end
|
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
|
test "reject regex shortcode", %{message: message} do
|
||||||
refute "firedfox" in installed()
|
refute "firedfox" in installed()
|
||||||
|
|
||||||
|
@ -118,7 +156,11 @@ test "reject if size is above the limit", %{message: message} do
|
||||||
refute "firedfox" in installed()
|
refute "firedfox" in installed()
|
||||||
|
|
||||||
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox.png"} ->
|
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)
|
end)
|
||||||
|
|
||||||
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 50_000)
|
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()
|
refute "firedfox" in installed()
|
||||||
|
|
||||||
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox.png"} ->
|
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)
|
end)
|
||||||
|
|
||||||
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
|
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
|
||||||
|
|
Loading…
Reference in a new issue