Bump upstream akkoma

Anonymous emoji and fetch security check tweaks got merged upstream.
This commit is contained in:
Oneric 2024-10-16 19:57:52 +02:00
parent 6006b4b992
commit e118a40563
5 changed files with 1 additions and 526 deletions

2
akkoma

@ -1 +1 @@
Subproject commit 3c72b48a05921b00e201965ad22b751c5d46bcdf Subproject commit f1018867097e6f293d8b2b5b6935f0a7ebf99bd0

View file

@ -1,66 +0,0 @@
From 4ff52930936699161223b12c4396d5a5ad6736d4 Mon Sep 17 00:00:00 2001
From: Oneric <oneric@oneric.stub>
Date: Sun, 23 Jun 2024 20:46:58 +0200
Subject: [PATCH] Federate emoji as anonymous objects
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Usually an id should point to another AP object
and the image file isnt an AP object. We currently
do not provide standalone AP objects for emoji and
don't keep track of remote emoji at all.
Thus just federate them as anonymous objects,
i.e. objects only existing within a parent context
and using an explicit null id.
IceShrimp.NET previously adopted anonymous objects
for remote emoji without any apparent issues. See:
https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/commit/333611f65eb2a65b2779ece0435b5ba84bf60e99
Fixes: https://akkoma.dev/AkkomaGang/akkoma/issues/694
---
lib/pleroma/web/activity_pub/transmogrifier.ex | 2 +-
.../web/activity_pub/transmogrifier/note_handling_test.exs | 2 +-
test/pleroma/web/activity_pub/views/user_view_test.exs | 2 +-
4 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index ca5e85f2e..75c1f0f0c 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -951,7 +951,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
- "id" => url
+ "id" => nil
}
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
index 16ee31483..c8aa2a1ed 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
@@ -700,7 +700,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
assert Transmogrifier.take_emoji_tags(user) == [
%{
"icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"},
- "id" => "https://example.org/firefox.png",
+ "id" => nil,
"name" => ":firefox:",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z"
diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs
index abe91cdea..2a367b680 100644
--- a/test/pleroma/web/activity_pub/views/user_view_test.exs
+++ b/test/pleroma/web/activity_pub/views/user_view_test.exs
@@ -43,7 +43,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
"tag" => [
%{
"icon" => %{"type" => "Image", "url" => "/test"},
- "id" => "/test",
+ "id" => nil,
"name" => ":bib:",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z"

View file

@ -1,310 +0,0 @@
From 940792f8ba8eae33a2cb48b313986b3674aa3a20 Mon Sep 17 00:00:00 2001
From: Oneric <oneric@oneric.stub>
Date: Sat, 13 Jul 2024 06:54:37 +0200
Subject: [PATCH 1/2] Refetch on AP ID mismatch
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
As hinted at in the commit message when strict checking
was added in 8684964c5d03f6c70f73730b3f1ad26784ffb004,
refetching is more robust than display URL comparison
but in exchange is harder to implement correctly.
A similar refetch approach is also employed by
e.g. Mastodon, IceShrimp and FireFish.
To make sure no checks can be bypassed by forcing
a refetch, id checking is placed at the very end.
This will fix:
- Peertube display URL arrays our transmogrifier fails to normalise
- non-canonical display URLs from alternative frontends
(theoretical; we didntt get any actual reports about this)
It will also be helpful in the planned key handling overhaul.
The modified user collision test was introduced in
https://git.pleroma.social/pleroma/pleroma/-/merge_requests/461
and unfortunately the issues this fixes arent public.
Afaict it was just meant to guard against someone serving
faked data belonging to an unrelated domain. Since we now
refetch and the id actually is mocked, lookup now succeeds
but will use the real data from the authorative server
making it unproblematic. Instead modify the fake data further
and make sure we dont end up using the spoofed version.
---
lib/pleroma/object/containment.ex | 57 +++++++++++++++---------
lib/pleroma/object/fetcher.ex | 42 +++++++++++------
test/pleroma/object/containment_test.exs | 35 +++++++--------
test/pleroma/object/fetcher_test.exs | 2 +-
test/support/http_request_mock.ex | 7 ++-
5 files changed, 87 insertions(+), 56 deletions(-)
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index 37bc20e4d..7b1cc37bd 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -12,8 +12,6 @@ defmodule Pleroma.Object.Containment do
spoofing, therefore removal of object containment functions is NOT recommended.
"""
- alias Pleroma.Web.ActivityPub.Transmogrifier
-
def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor
end
@@ -50,16 +48,39 @@ def get_object(_) do
defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
defp compare_uris(_id_uri, _other_uri), do: :error
- defp compare_uris_exact(uri, uri), do: :ok
+ defp uri_strip_slash(%URI{path: path} = uri) when is_binary(path),
+ do: %{uri | path: String.replace_suffix(path, "/", "")}
+
+ defp uri_strip_slash(uri), do: uri
+
+ # domain names are case-insensitive per spec (other parts of URIs arent necessarily)
+ defp uri_normalise_host(%URI{host: host} = uri) when is_binary(host),
+ do: %{uri | host: String.downcase(host, :ascii)}
+
+ defp uri_normalise_host(uri), do: uri
+
+ defp compare_uri_identities(uri, uri), do: :ok
+
+ defp compare_uri_identities(id_uri, other_uri) when is_binary(id_uri) and is_binary(other_uri),
+ do: compare_uri_identities(URI.parse(id_uri), URI.parse(other_uri))
- defp compare_uris_exact(%URI{} = id, %URI{} = other),
- do: compare_uris_exact(URI.to_string(id), URI.to_string(other))
+ defp compare_uri_identities(%URI{} = id, %URI{} = other) do
+ normid =
+ %{id | fragment: nil}
+ |> uri_strip_slash()
+ |> uri_normalise_host()
- defp compare_uris_exact(id_uri, other_uri)
- when is_binary(id_uri) and is_binary(other_uri) do
- norm_id = String.replace_suffix(id_uri, "/", "")
- norm_other = String.replace_suffix(other_uri, "/", "")
- if norm_id == norm_other, do: :ok, else: :error
+ normother =
+ %{other | fragment: nil}
+ |> uri_strip_slash()
+ |> uri_normalise_host()
+
+ # Conversion back to binary avoids issues from non-normalised deprecated authority field
+ if URI.to_string(normid) == URI.to_string(normother) do
+ :ok
+ else
+ :error
+ end
end
@doc """
@@ -93,21 +114,13 @@ def contain_origin(id, %{"attributedTo" => actor} = params),
def contain_origin(_id, _data), do: :ok
@doc """
- Check whether the fetch URL (after redirects) exactly (sans tralining slash) matches either
- the canonical ActivityPub id or the objects url field (for display URLs from *key and Mastodon)
+ Check whether the fetch URL (after redirects) is the
+ same location the canonical ActivityPub id points to.
Since this is meant to be used for fetches, anonymous or transient objects are not accepted here.
"""
- def contain_id_to_fetch(url, %{"id" => id} = data) when is_binary(id) do
- with {:id, :error} <- {:id, compare_uris_exact(id, url)},
- # "url" can be a "Link" object and this is checked before full normalisation
- display_url <- Transmogrifier.fix_url(data)["url"],
- true <- display_url != nil do
- compare_uris_exact(display_url, url)
- else
- {:id, :ok} -> :ok
- _ -> :error
- end
+ def contain_id_to_fetch(url, %{"id" => id}) when is_binary(id) do
+ compare_uri_identities(url, id)
end
def contain_id_to_fetch(_url, _data), do: :error
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index 937026e04..7f9a922aa 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -116,7 +116,7 @@ defp reinject_object(%Object{} = object, new_data) do
@doc "Assumes object already is in our database and refetches from remote to update (e.g. for polls)"
def refetch_object(%Object{data: %{"id" => id}} = object) do
with {:local, false} <- {:local, Object.local?(object)},
- {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
+ {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id, true),
{:id, true} <- {:id, new_data["id"] == id},
{:ok, object} <- reinject_object(object, new_data) do
{:ok, object}
@@ -253,14 +253,17 @@ defp maybe_date_fetch(headers, date) do
end
end
- @doc "Fetches arbitrary remote object and performs basic safety and authenticity checks"
- def fetch_and_contain_remote_object_from_id(id)
+ @doc """
+ Fetches arbitrary remote object and performs basic safety and authenticity checks.
+ When the fetch URL is known to already be a canonical AP id, checks are stricter.
+ """
+ def fetch_and_contain_remote_object_from_id(id, is_ap_id \\ false)
- def fetch_and_contain_remote_object_from_id(%{"id" => id}),
- do: fetch_and_contain_remote_object_from_id(id)
+ def fetch_and_contain_remote_object_from_id(%{"id" => id}, is_ap_id),
+ do: fetch_and_contain_remote_object_from_id(id, is_ap_id)
- def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
- Logger.debug("Fetching object #{id} via AP")
+ def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do
+ Logger.debug("Fetching object #{id} via AP [ap_id=#{is_ap_id}]")
with {:valid_uri_scheme, true} <- {:valid_uri_scheme, String.starts_with?(id, "http")},
%URI{} = uri <- URI.parse(id),
@@ -270,18 +273,31 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
{:mrf_accept_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri)},
{:local_fetch, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)},
{:ok, final_id, body} <- get_object(id),
+ # a canonical ID shouldn't be a redirect
+ true <- !is_ap_id || final_id == id,
{:ok, data} <- safe_json_decode(body),
- {_, :ok} <- {:strict_id, Containment.contain_id_to_fetch(final_id, data)},
- {_, :ok} <- {:containment, Containment.contain_origin(final_id, data)} do
+ {_, :ok} <- {:containment, Containment.contain_origin(final_id, data)},
+ {_, _, :ok} <- {:strict_id, data["id"], Containment.contain_id_to_fetch(final_id, data)} do
unless Instances.reachable?(final_id) do
Instances.set_reachable(final_id)
end
{:ok, data}
else
- {:strict_id, _} = e ->
- log_fetch_error(id, e)
- {:error, :id_mismatch}
+ # E.g. Mastodon and *key serve the AP object directly under their display URLs without
+ # redirecting to their canonical location first, thus ids will expectedly differ.
+ # Similarly keys, either use a fragment ID and are a subobjects or a distinct ID
+ # but for compatibility are still a subobject presenting their owning actors ID at the toplevel.
+ # Refetching _once_ from the listed id, should yield a strict match afterwards.
+ {:strict_id, ap_id, _} = e ->
+ case is_ap_id do
+ false ->
+ fetch_and_contain_remote_object_from_id(ap_id, true)
+
+ true ->
+ log_fetch_error(id, e)
+ {:error, :id_mismatch}
+ end
{:mrf_reject_check, _} = e ->
log_fetch_error(id, e)
@@ -311,7 +327,7 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
end
end
- def fetch_and_contain_remote_object_from_id(_id),
+ def fetch_and_contain_remote_object_from_id(_id, _is_ap_id),
do: {:error, :invalid_id}
defp check_crossdomain_redirect(final_host, original_url)
diff --git a/test/pleroma/object/containment_test.exs b/test/pleroma/object/containment_test.exs
index f8f40a3ac..1a1d01473 100644
--- a/test/pleroma/object/containment_test.exs
+++ b/test/pleroma/object/containment_test.exs
@@ -9,7 +9,6 @@ defmodule Pleroma.Object.ContainmentTest do
alias Pleroma.User
import Pleroma.Factory
- import ExUnit.CaptureLog
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -136,23 +135,17 @@ test "contain_id_to_fetch() allows matching IDs" do
)
end
- test "contain_id_to_fetch() allows display URLs" do
+ test "contain_id_to_fetch() allows fragments and normalises domain casing" do
data = %{
- "id" => "http://example.com/~alyssa/activities/1234.json",
- "url" => "http://example.com/@alyssa/status/1234"
+ "id" => "http://example.com/users/capybara",
+ "url" => "http://example.com/@capybara"
}
- :ok =
- Containment.contain_id_to_fetch(
- "http://example.com/@alyssa/status/1234",
- data
- )
-
- :ok =
- Containment.contain_id_to_fetch(
- "http://example.com/@alyssa/status/1234/",
- data
- )
+ assert :ok ==
+ Containment.contain_id_to_fetch(
+ "http://EXAMPLE.com/users/capybara#key",
+ data
+ )
end
test "users cannot be collided through fake direction spoofing attempts" do
@@ -164,10 +157,14 @@ test "users cannot be collided through fake direction spoofing attempts" do
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
})
- assert capture_log(fn ->
- {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
- end) =~
- "[error] Could not decode user at fetch https://n1u.moe/users/rye"
+ # Fetch from an attempted spoof id will suceed, but automatically retrieve
+ # the real data from the homeserver instead of naïvely using the spoof
+ {:ok, fetched_user} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
+
+ refute fetched_user.name == "evil rye"
+ refute fetched_user.raw_bio == "boooo!"
+ assert fetched_user.name == "♡ rye ♡"
+ assert fetched_user.nickname == "rye@niu.moe"
end
test "contain_origin_from_id() gracefully handles cases where no ID is present" do
diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs
index 12154cb05..d2de0ccc5 100644
--- a/test/pleroma/object/fetcher_test.exs
+++ b/test/pleroma/object/fetcher_test.exs
@@ -252,7 +252,7 @@ test "it does not fetch a spoofed object with wrong content type" do
end
test "it does not fetch a spoofed object with id different from URL" do
- assert {:error, :id_mismatch} =
+ assert {:error, :not_found} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
)
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 6a01393e3..ea06c4966 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -263,7 +263,12 @@ def get("https://n1u.moe/users/rye", _, _, @activitypub_accept_headers) do
{:ok,
%Tesla.Env{
status: 200,
- body: File.read!("test/fixtures/tesla_mock/rye.json"),
+ body:
+ File.read!("test/fixtures/tesla_mock/rye.json")
+ |> Jason.decode!()
+ |> Map.put("name", "evil rye")
+ |> Map.put("bio", "boooo!")
+ |> Jason.encode!(),
headers: activitypub_object_headers()
}}
end
--
2.39.5

View file

@ -1,144 +0,0 @@
From d5b0720596b70240bb559a2084d3c3cdc76fe7b2 Mon Sep 17 00:00:00 2001
From: Oneric <oneric@oneric.stub>
Date: Sat, 13 Jul 2024 06:54:45 +0200
Subject: [PATCH 2/2] Allow cross-domain redirects on AP requests
Since we now remember the final location redirects lead to
and use it for all further checks since
3e134b07fa4e382f1f4cfdbe90e74f8e73336a4e, these redirects
can no longer be exploited to serve counterfeit objects.
This fixes:
- display URLs from independent webapp clients
redirecting to the canonical domain
- Peertube display URLs for remote content
(acting like the above)
---
lib/pleroma/object/fetcher.ex | 18 +---------------
test/pleroma/object/fetcher_test.exs | 32 ++++++++++++++++++++++++----
2 files changed, 29 insertions(+), 21 deletions(-)
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index 7f9a922aa..2cecfec67 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -317,7 +317,7 @@ def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do
{:containment, reason} ->
log_fetch_error(id, reason)
- {:error, reason}
+ {:error, {:containment, reason}}
{:error, e} ->
{:error, e}
@@ -330,22 +330,10 @@ def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do
def fetch_and_contain_remote_object_from_id(_id, _is_ap_id),
do: {:error, :invalid_id}
- defp check_crossdomain_redirect(final_host, original_url)
-
# HOPEFULLY TEMPORARY
# Basically none of our Tesla mocks in tests set the (supposed to
# exist for Tesla proper) url parameter for their responses
# causing almost every fetch in test to fail otherwise
- if @mix_env == :test do
- defp check_crossdomain_redirect(nil, _) do
- {:cross_domain_redirect, false}
- end
- end
-
- defp check_crossdomain_redirect(final_host, original_url) do
- {:cross_domain_redirect, final_host != URI.parse(original_url).host}
- end
-
if @mix_env == :test do
defp get_final_id(nil, initial_url), do: initial_url
defp get_final_id("", initial_url), do: initial_url
@@ -371,10 +359,6 @@ def get_object(id) do
with {:ok, %{body: body, status: code, headers: headers, url: final_url}}
when code in 200..299 <-
HTTP.Backoff.get(id, headers),
- remote_host <-
- URI.parse(final_url).host,
- {:cross_domain_redirect, false} <-
- check_crossdomain_redirect(remote_host, id),
{:has_content_type, {_, content_type}} <-
{:has_content_type, List.keyfind(headers, "content-type", 0)},
{:parse_content_type, {:ok, "application", subtype, type_params}} <-
diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs
index d2de0ccc5..84bf0aa05 100644
--- a/test/pleroma/object/fetcher_test.exs
+++ b/test/pleroma/object/fetcher_test.exs
@@ -22,6 +22,7 @@ defp spoofed_object_with_ids(
|> Jason.decode!()
|> Map.put("id", id)
|> Map.put("actor", actor_id)
+ |> Map.put("attributedTo", actor_id)
|> Jason.encode!()
end
@@ -109,7 +110,7 @@ defp spoofed_object_with_ids(
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_media_redirect1")
}
- # Spoof: cross-domain redirect with final domain id
+ # Spoof: cross-domain redirect with final domain id, but original id actor
%{method: :get, url: "https://patch.cx/objects/spoof_media_redirect2"} ->
%Tesla.Env{
status: 200,
@@ -118,6 +119,19 @@ defp spoofed_object_with_ids(
body: spoofed_object_with_ids("https://media.patch.cx/objects/spoof_media_redirect2")
}
+ # No-Spoof: cross-domain redirect with id and actor from final domain
+ %{method: :get, url: "https://patch.cx/objects/spoof_media_redirect3"} ->
+ %Tesla.Env{
+ status: 200,
+ url: "https://media.patch.cx/objects/spoof_media_redirect3",
+ headers: [{"content-type", "application/activity+json"}],
+ body:
+ spoofed_object_with_ids(
+ "https://media.patch.cx/objects/spoof_media_redirect3",
+ "https://media.patch.cx/users/rin"
+ )
+ }
+
# No-Spoof: same domain redirect
%{method: :get, url: "https://patch.cx/objects/spoof_redirect"} ->
%Tesla.Env{
@@ -264,19 +278,29 @@ test "it does not fetch a spoofed object with id different from URL" do
end
test "it does not fetch an object via cross-domain redirects (initial id)" do
- assert {:error, {:cross_domain_redirect, true}} =
+ assert {:error, {:containment, _}} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/objects/spoof_media_redirect1"
)
end
- test "it does not fetch an object via cross-domain redirects (final id)" do
- assert {:error, {:cross_domain_redirect, true}} =
+ test "it does not fetch an object via cross-domain redirect if the actor is from the original domain" do
+ assert {:error, {:containment, :error}} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/objects/spoof_media_redirect2"
)
end
+ test "it allows cross-domain redirects when id and author are from final domain" do
+ assert {:ok, %{"id" => id, "attributedTo" => author}} =
+ Fetcher.fetch_and_contain_remote_object_from_id(
+ "https://patch.cx/objects/spoof_media_redirect3"
+ )
+
+ assert URI.parse(id).host == "media.patch.cx"
+ assert URI.parse(author).host == "media.patch.cx"
+ end
+
test "it accepts same-domain redirects" do
assert {:ok, %{"id" => id} = _object} =
Fetcher.fetch_and_contain_remote_object_from_id(
--
2.39.5

View file

@ -13,8 +13,3 @@ pr814_10_fix-swagger-ui.patch
pr642_receive-mathml.patch pr642_receive-mathml.patch
# Avoid unecessary work # Avoid unecessary work
pr582_dont-fetch-remote-uris-on-unauthenticated-search.patch pr582_dont-fetch-remote-uris-on-unauthenticated-search.patch
# Fixes some edge cases in old "id or display url" check; more robust
pr819_01_refetch-on-AP-ID-mismatch.patch
pr819_02_allow-cross-domain-redirects-on-AP-requests.patch
# Don't federate bogus emoji ID; just use anonymous object
pr815_federate-anonymous-emoji.patch