From 80e72b79f57bad270c530d94527f760c14d8c152 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 30 Apr 2026 14:31:06 +0400 Subject: [PATCH] Add spoofing regression tests --- .gitignore | 3 + .../activity_pub_controller_test.exs | 68 +++++ .../update_handling_test.exs | 26 ++ ...mapped_signature_to_identity_plug_test.exs | 10 +- test/pleroma/workers/receiver_worker_test.exs | 239 ++++++++++++++++++ 5 files changed, 342 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 355cea069..d8e5ed553 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ pleroma.iml # asdf .tool-versions +# mise +mise.toml + # Editor temp files *~ *# 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 d5947186f..42cb35669 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -726,6 +726,74 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert Activity.get_by_ap_id(data["id"]) end + test "does not create a forged post after failed signature retry", %{conn: conn} do + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + object_id = "https://example.com/objects/inbox-forged-note" + + data = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-forged-create", + "context" => "https://example.com/contexts/inbox-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/inbox-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + refute Object.get_by_ap_id(object_id) + end + + test "does not create a forged like after failed signature retry", %{conn: conn} do + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + data = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + end + test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs index 94c502ad6..f04f9cc61 100644 --- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -64,6 +64,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:ok, _update, _} = ObjectValidator.validate(update, []) end + + test "returns an error if the remote update target is unknown" do + remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice") + + update = %{ + "type" => "Update", + "actor" => remote_user.ap_id, + "id" => "https://example.com/activities/update-unknown-object", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://example.com/objects/unknown", + "actor" => remote_user.ap_id, + "content" => "edited content", + "published" => "2024-07-25T13:33:31Z", + "updated" => "2024-07-25T13:34:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false) + refute cng.valid? + assert Keyword.has_key?(cng.errors, :object) + end end describe "update note" do diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index 33eff1bc5..81c6b0c5d 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -47,13 +47,15 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do assert %{valid_signature: false} == conn.assigns end - @tag skip: "known breakage; the testsuite presently depends on it" test "it considers a mapped identity to be invalid when the identity cannot be found" do + actor = "http://niu.moe/users/rye" + conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://niu.moe/users/rye") + build_conn(:post, "/doesntmattter", %{"actor" => actor}) + |> set_signature(actor) |> MappedSignatureToIdentityPlug.call(%{}) - assert %{valid_signature: false} == conn.assigns + assert conn.assigns.valid_signature == false + refute Map.has_key?(conn.assigns, :user) end end diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index bc027ad4c..9dccd739b 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -14,6 +14,43 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker + defp mismatched_signature_headers do + [ + {"host", "example.com"}, + {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, + {"digest", "SHA-256=fake-digest"}, + {"content-type", "application/activity+json"}, + { + "signature", + "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + } + ] + end + + defp assert_mismatched_signature_cancelled(params) do + with_mocks [ + {Pleroma.Signature, [:passthrough], + [ + refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, + validate_signature: fn _conn -> true end + ]}, + {Pleroma.Web.Federator, [:passthrough], + [perform: fn :incoming_ap_doc, _params -> {:ok, :processed} end]} + ] do + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: params, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute called(Pleroma.Web.Federator.perform(:incoming_ap_doc, :_)) + end + end + test "it does not retry MRF reject" do params = insert(:note).data @@ -346,4 +383,206 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end end + + test "Federator preserves request metadata needed for ReceiverWorker signature checks" do + params = insert(:note_activity).data + + req_headers = [ + {"host", "example.com"}, + {"signature", "keyId=\"https://example.com/users/alice#main-key\""} + ] + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: req_headers, + request_path: "/inbox", + params: params, + query_string: "foo=bar" + }) + + assert %{ + "method" => "POST", + "req_headers" => ^req_headers, + "request_path" => "/inbox", + "params" => ^params, + "query_string" => "foo=bar" + } = oban_job.args + end + + test "cancels signature actor mismatch through Federator-created jobs" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + note = insert(:note, user: bob, object_local: false) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/federator-malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + assert_mismatched_signature_cancelled(update) + end + + test "cancels signature actor mismatch before processing a forged Create" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://example.com/objects/forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create) + end + + test "cancels signature actor mismatch before actually creating a forged post" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + object_id = "https://example.com/objects/actually-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/actually-forged-create", + "context" => "https://example.com/contexts/actually-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/actually-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: create, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute Pleroma.Object.get_by_ap_id(object_id) + end + + test "cancels signature actor mismatch before processing a forged Like" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(like) + end + + test "cancels signature actor mismatch before actually creating a forged Like" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/actually-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: like, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute Pleroma.Activity.get_by_ap_id(like["id"]) + end + + test "cancels signature actor mismatch before processing a forged Announce" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + announce = %{ + "type" => "Announce", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(announce) + end + + test "cancels signature actor mismatch before processing a forged Follow" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + followed = insert(:user) + + follow = %{ + "type" => "Follow", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-follow", + "to" => [followed.ap_id], + "cc" => [], + "object" => followed.ap_id + } + + assert_mismatched_signature_cancelled(follow) + end + + test "cancels signature actor mismatch before processing a forged Undo" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + undo = %{ + "type" => "Undo", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-undo", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => "https://example.com/activities/existing-bob-activity" + } + + assert_mismatched_signature_cancelled(undo) + end end