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.
This commit is contained in:
Lain Soykaf 2026-10-30 10:20:06 +04:00
commit 44adf271c2
No known key found for this signature in database
2 changed files with 41 additions and 0 deletions

View file

@ -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(

View file

@ -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