diff --git a/CHANGELOG.md b/CHANGELOG.md index 723c1060f..6a42edffc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Fixed - Issue preventing fetching anything from IPv6-only instances - Issue allowing post content to leak via opengraph tags despite :estrict\_unauthenticated being set +- Scope misdetection of remote posts resulting from not recognising + JSON-LD-compacted forms of public scope; affected e.g. federation with bovine ## 2024.03 diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 033fc9e78..065df5150 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -58,21 +58,48 @@ def fix_summary(%{"summary" => _} = object) do def fix_summary(object), do: Map.put(object, "summary", "") - def fix_addressing_list(map, field) do - addrs = map[field] - + defp fix_addressing_list(addrs) do cond do - is_list(addrs) -> - Map.put(map, field, Enum.filter(addrs, &is_binary/1)) - - is_binary(addrs) -> - Map.put(map, field, [addrs]) - - true -> - Map.put(map, field, []) + is_list(addrs) -> Enum.filter(addrs, &is_binary/1) + is_binary(addrs) -> [addrs] + true -> [] end end + # Due to JSON-LD simply "Public" and "as:Public" are equivalent to the full URI + # but to simplify later checks we only want to deal with one reperesentation internally + defp normalise_addressing_public_list(map, all_fields) + + defp normalise_addressing_public_list(%{} = map, [field | fields]) do + full_uri = Pleroma.Constants.as_public() + + map = + if map[field] != nil do + new_fval = + map[field] + |> fix_addressing_list() + |> Enum.map(fn + "Public" -> full_uri + "as:Public" -> full_uri + x -> x + end) + + Map.put(map, field, new_fval) + else + map + end + + normalise_addressing_public_list(map, fields) + end + + defp normalise_addressing_public_list(map, _) do + map + end + + defp normalise_addressing_public(map) do + normalise_addressing_public_list(map, ["to", "cc", "bto", "bcc"]) + end + # if directMessage flag is set to true, leave the addressing alone def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection), do: object @@ -96,6 +123,10 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collect |> Map.put("cc", final_cc) end + def fix_addressing_list_key(map, field) do + Map.put(map, field, fix_addressing_list(map[field])) + end + def fix_addressing(object) do {:ok, %User{follower_address: follower_collection}} = object @@ -103,10 +134,10 @@ def fix_addressing(object) do |> User.get_or_fetch_by_ap_id() object - |> fix_addressing_list("to") - |> fix_addressing_list("cc") - |> fix_addressing_list("bto") - |> fix_addressing_list("bcc") + |> fix_addressing_list_key("to") + |> fix_addressing_list_key("cc") + |> fix_addressing_list_key("bto") + |> fix_addressing_list_key("bcc") |> fix_explicit_addressing(follower_collection) |> CommonFixes.fix_implicit_addressing(follower_collection) end @@ -383,11 +414,28 @@ defp get_reported(objects) do end) end - def handle_incoming(data, options \\ []) + def handle_incoming(data, options \\ []) do + data = normalise_addressing_public(data) + + data = + if data["object"] != nil do + object = normalise_addressing_public(data["object"]) + Map.put(data, "object", object) + else + data + end + + handle_incoming_normalised(data, options) + end + + defp handle_incoming_normalised(data, options) # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them # with nil ID. - def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do + defp handle_incoming_normalised( + %{"type" => "Flag", "object" => objects, "actor" => actor} = data, + _options + ) do with context <- data["context"] || Utils.generate_context_id(), content <- data["content"] || "", %User{} = actor <- User.get_cached_by_ap_id(actor), @@ -408,20 +456,21 @@ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = end # disallow objects with bogus IDs - def handle_incoming(%{"id" => nil}, _options), do: :error - def handle_incoming(%{"id" => ""}, _options), do: :error + defp handle_incoming_normalised(%{"id" => nil}, _options), do: :error + defp handle_incoming_normalised(%{"id" => ""}, _options), do: :error # length of https:// = 8, should validate better, but good enough for now. - def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8, - do: :error + defp handle_incoming_normalised(%{"id" => id}, _options) + when is_binary(id) and byte_size(id) < 8, + do: :error - @doc "Rewrite misskey likes into EmojiReacts" - def handle_incoming( - %{ - "type" => "Like", - "content" => reaction - } = data, - options - ) do + # Rewrite misskey likes into EmojiReacts + defp handle_incoming_normalised( + %{ + "type" => "Like", + "content" => reaction + } = data, + options + ) do if Pleroma.Emoji.is_unicode_emoji?(reaction) || Pleroma.Emoji.matches_shortcode?(reaction) do data |> Map.put("type", "EmojiReact") @@ -433,11 +482,11 @@ def handle_incoming( end end - def handle_incoming( - %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, - options - ) - when objtype in ~w{Question Answer Audio Video Event Article Note Page} do + defp handle_incoming_normalised( + %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, + options + ) + when objtype in ~w{Question Answer Audio Video Event Article Note Page} do fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) object = @@ -469,8 +518,8 @@ def handle_incoming( end end - def handle_incoming(%{"type" => type} = data, _options) - when type in ~w{Like EmojiReact Announce Add Remove} do + defp handle_incoming_normalised(%{"type" => type} = data, _options) + when type in ~w{Like EmojiReact Announce Add Remove} do with :ok <- ObjectValidator.fetch_actor_and_object(data), {:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} @@ -480,11 +529,11 @@ def handle_incoming(%{"type" => type} = data, _options) end end - def handle_incoming( - %{"type" => type} = data, - _options - ) - when type in ~w{Update Block Follow Accept Reject} do + defp handle_incoming_normalised( + %{"type" => type} = data, + _options + ) + when type in ~w{Update Block Follow Accept Reject} do with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do @@ -492,10 +541,10 @@ def handle_incoming( end end - def handle_incoming( - %{"type" => "Delete"} = data, - _options - ) do + defp handle_incoming_normalised( + %{"type" => "Delete"} = data, + _options + ) do with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} @@ -515,15 +564,15 @@ def handle_incoming( end end - def handle_incoming( - %{ - "type" => "Undo", - "object" => %{"type" => "Follow", "object" => followed}, - "actor" => follower, - "id" => id - } = _data, - _options - ) do + defp handle_incoming_normalised( + %{ + "type" => "Undo", + "object" => %{"type" => "Follow", "object" => followed}, + "actor" => follower, + "id" => id + } = _data, + _options + ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do @@ -534,28 +583,28 @@ def handle_incoming( end end - def handle_incoming( - %{ - "type" => "Undo", - "object" => %{"type" => type} - } = data, - _options - ) - when type in ["Like", "EmojiReact", "Announce", "Block"] do + defp handle_incoming_normalised( + %{ + "type" => "Undo", + "object" => %{"type" => type} + } = data, + _options + ) + when type in ["Like", "EmojiReact", "Announce", "Block"] do with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} end end # For Undos that don't have the complete object attached, try to find it in our database. - def handle_incoming( - %{ - "type" => "Undo", - "object" => object - } = activity, - options - ) - when is_binary(object) do + defp handle_incoming_normalised( + %{ + "type" => "Undo", + "object" => object + } = activity, + options + ) + when is_binary(object) do with %Activity{data: data} <- Activity.get_by_ap_id(object) do activity |> Map.put("object", data) @@ -565,15 +614,15 @@ def handle_incoming( end end - def handle_incoming( - %{ - "type" => "Move", - "actor" => origin_actor, - "object" => origin_actor, - "target" => target_actor - }, - _options - ) do + defp handle_incoming_normalised( + %{ + "type" => "Move", + "actor" => origin_actor, + "object" => origin_actor, + "target" => target_actor + }, + _options + ) do with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor), # Use a dramatically shortened maximum age before refresh here because it is reasonable # for a user to @@ -588,7 +637,7 @@ def handle_incoming( end end - def handle_incoming(_, _), do: :error + defp handle_incoming_normalised(_, _), do: :error @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil def get_obj_helper(id, options \\ []) do diff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs index 76a7a6d37..d3cc239cf 100644 --- a/test/pleroma/web/federator_test.exs +++ b/test/pleroma/web/federator_test.exs @@ -137,6 +137,37 @@ test "successfully processes incoming AP docs with correct origin" do assert {:error, :already_present} = ObanHelpers.perform(job) end + test "successfully normalises public scope descriptors" do + params = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "http://mastodon.example.org/users/admin", + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/activities/1", + "object" => %{ + "type" => "Note", + "content" => "hi world!", + "id" => "http://mastodon.example.org/users/admin/objects/1", + "attributedTo" => "http://mastodon.example.org/users/admin", + "to" => ["Public"] + }, + "to" => ["as:Public"] + } + + assert {:ok, job} = Federator.incoming_ap_doc(params) + assert {:ok, activity} = ObanHelpers.perform(job) + assert activity.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + + object = + from( + object in Pleroma.Object, + where: fragment("(?)->>'id' = ?", object.data, ^activity.data["object"]), + limit: 1 + ) + |> Repo.one() + + assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + end + test "rejects incoming AP docs with incorrect origin" do params = %{ "@context" => "https://www.w3.org/ns/activitystreams",