From b3887a6fa775bda1f51efab4774cba838ad0db91 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 2 Dec 2025 23:25:42 +0100 Subject: [PATCH] AP C2S: Validate visibility for C2S requests to /users/:nickname/outbox A local user could previously send Announce/EmojiReact/Like activities to their outbox referencing objects that aren't visible to them and they would get processed as if can see them. Only requirement is knowing the URI of the object and the users instance having C2S enabled (currently disabled by default). --- .../activity_pub/activity_pub_controller.ex | 21 ++++- .../activity_pub_controller_test.exs | 76 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index daf0d38e6..3d126e7d3 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -482,6 +482,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {:ok, activity} end + defp validate_visibility(%User{} = user, %{"type" => type, "object" => object} = activity) do + with {_, %Object{} = normalized_object} <- {:normalize, Object.normalize(object, fetch: false)}, + {_, true} <- {:visibility, Visibility.visible_for_user?(normalized_object, user)} do + {:ok, activity} + else + {:normalize, _} -> + if user.local and type == "Create" do + # Creating new object via C2S + {:ok, activity} + else + {:error, "No such object found"} + end + + {:visibility, _} -> + {:forbidden, "You can't interact with this object"} + end + end + def update_outbox( %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn, %{"nickname" => nickname} = params @@ -493,7 +511,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> Map.put("actor", actor) with {:ok, params} <- fix_user_message(user, params), - {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true), + {:ok, activity} <- validate_visibility(user, params), + {:ok, activity, _} <- Pipeline.common_pipeline(activity, local: true), %Activity{data: activity_data} <- Activity.normalize(activity) do conn |> put_status(:created) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 2b8418dcd..adb65431c 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -1706,6 +1706,82 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert note_object == Object.normalize(note_activity, fetch: false) end + test "it rejects like activity to object invisible to actor", %{conn: conn} do + user = insert(:user) + stranger = insert(:user, local: true) + {:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"}) + + assert Pleroma.Web.ActivityPub.Visibility.private?(post) + + post_object = Object.normalize(post, fetch: false) + + data = %{ + type: "Like", + object: %{ + id: post_object.data["id"] + } + } + + conn = + conn + |> assign(:user, stranger) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{stranger.nickname}/outbox", data) + + assert json_response(conn, 403) + end + + test "it rejects announce activity to object invisible to actor", %{conn: conn} do + user = insert(:user) + stranger = insert(:user, local: true) + {:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"}) + + assert Pleroma.Web.ActivityPub.Visibility.private?(post) + + post_object = Object.normalize(post, fetch: false) + + data = %{ + type: "Announce", + object: %{ + id: post_object.data["id"] + } + } + + conn = + conn + |> assign(:user, stranger) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{stranger.nickname}/outbox", data) + + assert json_response(conn, 403) + end + + test "it rejects emojireact activity to object invisible to actor", %{conn: conn} do + user = insert(:user) + stranger = insert(:user, local: true) + {:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"}) + + assert Pleroma.Web.ActivityPub.Visibility.private?(post) + + post_object = Object.normalize(post, fetch: false) + + data = %{ + type: "EmojiReact", + object: %{ + id: post_object.data["id"] + }, + content: "😀" + } + + conn = + conn + |> assign(:user, stranger) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{stranger.nickname}/outbox", data) + + assert json_response(conn, 403) + end + test "it increases like count when receiving a like action", %{conn: conn} do note_activity = insert(:note_activity) note_object = Object.normalize(note_activity, fetch: false)