fix: reject duplicate Signature headers

This commit is contained in:
Lain Soykaf 2026-07-26 12:26:35 +04:00
commit 9df50eea82
No known key found for this signature in database
3 changed files with 97 additions and 14 deletions

View file

@ -1 +1 @@
Reject unsupported RFC 9421 inbox signatures instead of queueing a doomed failed-signature retry, allowing clients to fall back to legacy HTTP Signatures.
Reject unsupported or ambiguous inbox signatures instead of queueing a doomed failed-signature retry, allowing clients to fall back to legacy HTTP Signatures.

View file

@ -309,7 +309,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
} = conn,
%{"nickname" => nickname} = params
) do
with {:recipient_exists, %User{} = recipient} <-
with false <- unsupported_http_message_signature?(conn),
{:recipient_exists, %User{} = recipient} <-
{:recipient_exists, User.get_cached_by_nickname(nickname)},
{:sender_exists, {:ok, %User{} = actor}} <-
{:sender_exists, User.get_or_fetch_by_ap_id(params["actor"])},
@ -320,6 +321,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
Federator.incoming_ap_doc(params)
json(conn, "ok")
else
true ->
reject_unsupported_http_message_signature(conn)
{:recipient_exists, _} ->
conn
|> put_status(:not_found)
@ -348,15 +352,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
if unsupported_http_message_signature?(conn) do
reject_unsupported_http_message_signature(conn)
else
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
end
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
if unsupported_http_message_signature?(conn) do
conn
|> put_status(:unauthorized)
|> json("error, unsupported HTTP Message Signature")
reject_unsupported_http_message_signature(conn)
else
Federator.incoming_failed_signature_ap_doc(%{
method: conn.method,
@ -387,15 +393,25 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> json("error, missing HTTP Signature")
end
defp unsupported_http_message_signature?(conn) do
Plug.Conn.get_req_header(conn, "signature-input") != [] and
not cavage_signature?(conn)
defp reject_unsupported_http_message_signature(conn) do
conn
|> put_status(:unauthorized)
|> json("error, unsupported HTTP Message Signature")
end
defp cavage_signature?(conn) do
conn
|> Plug.Conn.get_req_header("signature")
|> Enum.any?(&Regex.match?(~r/(^|,)\s*keyId\s*=/, &1))
defp unsupported_http_message_signature?(conn) do
signature_headers = Plug.Conn.get_req_header(conn, "signature")
multiple_signature_headers?(signature_headers) or
(Plug.Conn.get_req_header(conn, "signature-input") != [] and
not cavage_signature?(signature_headers))
end
defp multiple_signature_headers?([_, _ | _]), do: true
defp multiple_signature_headers?(_), do: false
defp cavage_signature?(signature_headers) do
Enum.any?(signature_headers, &Regex.match?(~r/(^|,)\s*keyId\s*=/, &1))
end
defp post_inbox_relayed_create(conn, params) do

View file

@ -729,6 +729,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"])
end
test "rejects duplicate Signature headers before processing signed inbox requests", %{
conn: conn
} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> Map.update!(:req_headers, &[{"signature", "sig1=:fake-signature:"} | &1])
|> post("/inbox", data)
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
assert [] == all_enqueued(worker: ReceiverWorker)
end
test "it inserts an incoming activity into the database" <>
"even if we can't fetch the user but have it in our db",
%{conn: conn} do
@ -794,6 +810,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert [] == all_enqueued(worker: SignatureRetryWorker)
end
test "rejects duplicate Signature headers so clients can fall back", %{conn: conn} do
data = %{
"type" => "Create",
"actor" => "https://activitypubbot.example/user/ok",
"id" => "https://activitypubbot.example/activities/duplicate-signature-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://activitypubbot.example/objects/duplicate-signature-note",
"actor" => "https://activitypubbot.example/user/ok",
"attributedTo" => "https://activitypubbot.example/user/ok",
"content" => "hello from activitypub-bot",
"published" => "2026-05-29T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
legacy_signature =
"keyId=\"https://activitypubbot.example/user/ok#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
conn =
conn
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT")
|> put_req_header("digest", "SHA-256=fake-digest")
|> put_req_header("signature", "sig1=:fake-signature:")
|> Map.update!(:req_headers, &[{"signature", legacy_signature} | &1])
|> post("/inbox", data)
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
assert [] == all_enqueued(worker: SignatureRetryWorker)
end
test "keeps failed-signature retry for legacy signatures with Signature-Input", %{conn: conn} do
data = %{
"type" => "Create",
@ -1397,6 +1450,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"])
end
test "rejects duplicate Signature headers", %{conn: conn, data: data} do
user = insert(:user)
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> Map.update!(:req_headers, &[{"signature", "sig1=:fake-signature:"} | &1])
|> post("/users/#{user.nickname}/inbox", data)
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
assert [] == all_enqueued(worker: ReceiverWorker)
end
test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
user = insert(:user)