From 44adf271c24f354694fc53bd53b298169adb4859 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 30 Oct 2026 10:20:06 +0400 Subject: [PATCH 1/3] fix(activitypub): rate limit C2S outbox posts POST /users/:nickname/outbox had no rate limiter, so a client could send unlimited activities through the C2S API. Apply :statuses_actions to update_outbox. RateLimiter keys buckets by limiter name and user, so the outbox shares one counter with the Mastodon API status actions and mixing both APIs does not double the limit. --- .../activity_pub/activity_pub_controller.ex | 4 ++ .../activity_pub_controller_test.exs | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index dd5f6028f..5ee93eabf 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -26,6 +26,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug alias Pleroma.Web.Plugs.FederatingPlug alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter require Logger @@ -66,6 +67,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do when action in [:followers, :following] ) + # Shares the per-user bucket with Mastodon API status actions and emoji reactions. + plug(RateLimiter, [name: :statuses_actions] when action == :update_outbox) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media]) plug( 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 606d47c8d..b72fb6424 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -2685,6 +2685,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert result == "Character limit (5 characters) exceeded, contains 11 characters" end + + test "it is rate limited", %{conn: conn, activity: activity} do + clear_config([:rate_limit, :statuses_actions], {60_000, 2}) + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + + for _ <- 1..2 do + conn + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(201) + end + + assert conn + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(429) == %{"error" => "Throttled"} + end + + test "it shares the rate limit with the Mastodon API", %{activity: activity} do + clear_config([:rate_limit, :statuses_actions], {60_000, 2}) + %{user: user, conn: conn} = oauth_access(["write"]) + + for _ <- 1..2 do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{status: "cofe"}) + |> json_response(200) + end + + assert conn + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(429) == %{"error" => "Throttled"} + end end describe "/relay/followers" do From a4f1300578ef8a7f2135213ca0bacd9d561cdf4f Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 30 Oct 2026 10:20:43 +0400 Subject: [PATCH 2/3] fix(pleroma_api): rate limit emoji reactions The emoji reaction create and delete endpoints had no rate limiter. Apply :statuses_actions like the other status actions, plus a per-status :status_id_action bucket ("status_id_action:emoji_react") in the same way favourite and reblog do. --- .../controllers/emoji_reaction_controller.ex | 9 ++++ .../emoji_reaction_controller_test.exs | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex index 82abfce7f..57e3a2d5b 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete]) @@ -22,6 +23,14 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do when action == :index ) + plug( + RateLimiter, + [name: :status_id_action, bucket_name: "status_id_action:emoji_react", params: [:id]] + when action in [:create, :delete] + ) + + plug(RateLimiter, [name: :statuses_actions] when action in [:create, :delete]) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.EmojiReactionOperation action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs index 8fcdb233b..12b037a21 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -477,4 +477,53 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do |> get("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈") |> json_response_and_validate_schema(404) == %{"error" => "Record not found"} end + + describe "rate limiting" do + test "PUT and DELETE share a per-status limit", %{conn: conn} do + clear_config([:rate_limit, :status_id_action], {60_000, 2}) + user = insert(:user) + %{id: activity_id} = insert(:note_activity) + %{id: other_activity_id} = insert(:note_activity) + conn = prepare_conn_of_user(conn, user) + + conn + |> put("/api/v1/pleroma/statuses/#{activity_id}/reactions/☕") + |> json_response_and_validate_schema(200) + + conn + |> delete("/api/v1/pleroma/statuses/#{activity_id}/reactions/☕") + |> json_response_and_validate_schema(200) + + assert conn + |> put("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈") + |> json_response(429) == %{"error" => "Throttled"} + + conn + |> put("/api/v1/pleroma/statuses/#{other_activity_id}/reactions/🐈") + |> json_response_and_validate_schema(200) + end + + test "PUT and DELETE count against statuses_actions", %{conn: conn} do + clear_config([:rate_limit, :statuses_actions], {60_000, 2}) + user = insert(:user) + [first, second, third] = insert_list(3, :note_activity) + conn = prepare_conn_of_user(conn, user) + + conn + |> put("/api/v1/pleroma/statuses/#{first.id}/reactions/☕") + |> json_response_and_validate_schema(200) + + conn + |> delete("/api/v1/pleroma/statuses/#{first.id}/reactions/☕") + |> json_response_and_validate_schema(200) + + assert conn + |> put("/api/v1/pleroma/statuses/#{second.id}/reactions/☕") + |> json_response(429) == %{"error" => "Throttled"} + + assert conn + |> delete("/api/v1/pleroma/statuses/#{third.id}/reactions/☕") + |> json_response(429) == %{"error" => "Throttled"} + end + end end From 924df2a0633bfe5dc5213895e56460dcc95ca0a1 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 30 Oct 2026 10:20:44 +0400 Subject: [PATCH 3/3] chore: add changelog entry for C2S rate limits --- changelog.d/c2s-rate-limit.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/c2s-rate-limit.security diff --git a/changelog.d/c2s-rate-limit.security b/changelog.d/c2s-rate-limit.security new file mode 100644 index 000000000..8cab4a309 --- /dev/null +++ b/changelog.d/c2s-rate-limit.security @@ -0,0 +1 @@ +Rate limit ActivityPub C2S outbox posts and emoji reactions