From ea886dc36b3727d7bd22ede1bdb90de3dfb8668e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 12 May 2026 11:37:01 +0200 Subject: [PATCH 01/11] EnsureHostMatchesPlug: Ensure Host header matches instance URI --- changelog.d/host-header-verification.security | 1 + .../web/plugs/ensure_host_matches_plug.ex | 68 +++++++++++++++++++ lib/pleroma/web/router.ex | 1 + 3 files changed, 70 insertions(+) create mode 100644 changelog.d/host-header-verification.security create mode 100644 lib/pleroma/web/plugs/ensure_host_matches_plug.ex diff --git a/changelog.d/host-header-verification.security b/changelog.d/host-header-verification.security new file mode 100644 index 000000000..11a0784b2 --- /dev/null +++ b/changelog.d/host-header-verification.security @@ -0,0 +1 @@ +Ensure Host header is present and matches instance URI diff --git a/lib/pleroma/web/plugs/ensure_host_matches_plug.ex b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex new file mode 100644 index 000000000..87c4c0531 --- /dev/null +++ b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do + @moduledoc "Ensures Host header matches instance" + + alias Pleroma.Web.Endpoint + + import Plug.Conn + + def init(options), do: options + + @spec call(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t() + def call(%Plug.Conn{assigns: %{valid_signature: true}} = conn, _opts) do + # Host header is scheme-less, URI.parse needs the // + host_header = get_req_header(conn, "host") + host_uri = URI.parse("//#{host_header}") + instance_uri = URI.parse(Endpoint.url()) + instance_scheme_port = URI.default_port(instance_uri.scheme) + + case host_header do + [host] -> + cond do + host == "" -> + resp(conn, 400, "Host header not provided") |> halt() + + true -> + if host_matches?(host_uri, instance_uri, instance_scheme_port), + do: assign(conn, :valid_host_header, true), + else: resp(conn, 400, "Host header does not match this instance") |> halt() + end + + [_head | _rest] -> + conn + |> resp(400, "More than one Host header provided") + |> halt() + + [] -> + conn + |> resp(400, "Host header not provided") + |> halt() + end + end + + # Host header may not be provided, but signature verification failed anyway + def call(conn, _opts), do: conn + + defp case_insensitive_compare(checked, authority) do + String.downcase(checked) == String.downcase(authority) + end + + # Host header did not provide port + # Host header is scheme-less, URI.parse does not provide default port + defp host_matches?(%URI{host: req_host, port: nil}, %URI{host: instance_host}, _), + do: case_insensitive_compare(req_host, instance_host) + + # Host header provided a port, reverse proxy configuration (port cannot match Endpoint port) + # Both port 80 and 443 are valid based on Endpoint configuration + defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host}, port), + do: case_insensitive_compare(req_host, instance_host) + + # Host header provided port, configuration without reverse proxy (port matches Endpoint port) + defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host, port: port}, _), + do: case_insensitive_compare(req_host, instance_host) + + defp host_matches?(_, _, _), do: false +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c91ca8c97..813c5c482 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -216,6 +216,7 @@ defmodule Pleroma.Web.Router do pipeline :http_signature do plug(Pleroma.Web.Plugs.HTTPSignaturePlug) plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) + plug(Pleroma.Web.Plugs.EnsureHostMatchesPlug) end pipeline :inbox_guard do From d6d0ce72604ee450560916bb29297188579faa0e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 12 May 2026 14:00:28 +0200 Subject: [PATCH 02/11] EnsureHostMatchesPlug: Add tests --- .../plugs/ensure_host_matches_plug_test.exs | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 test/pleroma/web/plugs/ensure_host_matches_plug_test.exs diff --git a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs new file mode 100644 index 000000000..fbebc21d9 --- /dev/null +++ b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs @@ -0,0 +1,123 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Plugs.EnsureHostMatchesPlug + + import Mock + import Plug.Conn + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp set_host(conn, host), do: %{conn | req_headers: conn.req_headers ++ [{"host", host}]} + + describe "EnsureHostMatchesPlug" do + setup do + conn = build_conn(:post, "/cofe") |> assign(:valid_signature, true) + [conn: conn] + end + + test "gracefully handles no Host header", %{conn: conn} do + conn = EnsureHostMatchesPlug.call(conn, %{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "Host header not provided" + end + + test "gracefully handles empty Host header", %{conn: conn} do + conn = + conn + |> set_host("") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "Host header not provided" + end + + test "it rejects Host header not matching Endpoint URL", %{conn: conn} do + conn = + conn + |> set_host("invalid.example.com") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "Host header does not match this instance" + end + + test "it rejects Host header not matching Endpoint port", %{conn: conn} do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("#{endpoint.host}:25") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "Host header does not match this instance" + end + + test "it rejects multiple Host headers", %{conn: conn} do + conn = + conn + |> set_host("host1.example.com") + |> set_host("host2.example.com") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "More than one Host header provided" + end + + test "it works for Host header with port as 80", %{conn: conn} do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("#{endpoint.host}:80") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.halted == false + assert Map.get(conn.assigns, :valid_host_header, nil) + end + + test "it works for Host header with port as 443", %{conn: conn} do + with_mock Pleroma.Web.Endpoint, url: fn -> "https://localhost:4001" end do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("#{endpoint.host}:443") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.halted == false + assert Map.get(conn.assigns, :valid_host_header, nil) + end + end + + test "it works for Host header with port as same as Endpoint (no reverse proxy config)", %{ + conn: conn + } do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("#{endpoint.host}:#{endpoint.port}") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.halted == false + assert Map.get(conn.assigns, :valid_host_header, nil) + end + end +end From 90e390e45b1c1af4bbc8e2e68affb41fd621ea3e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 12 May 2026 16:48:49 +0200 Subject: [PATCH 03/11] fix tests --- test/support/conn_case.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index f010fec33..c01516169 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -119,7 +119,10 @@ defmodule Pleroma.Web.ConnCase do DataCase.stub_pipeline() Mox.verify_on_exit!() + endpoint = URI.parse(Pleroma.Web.Endpoint.url()) + conn = Phoenix.ConnTest.build_conn() + conn = %{conn | req_headers: [{"host", "#{endpoint.host}:#{endpoint.port}"}]} - {:ok, conn: Phoenix.ConnTest.build_conn()} + {:ok, conn: conn} end end From 35b5447f3f21a0a30370c86efffee531f56f1ea7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 12 May 2026 17:02:28 +0200 Subject: [PATCH 04/11] EnsureHostMatchesPlug: Add more tests --- .../plugs/ensure_host_matches_plug_test.exs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs index fbebc21d9..67252e7a7 100644 --- a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs +++ b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs @@ -55,6 +55,19 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do assert conn.resp_body == "Host header does not match this instance" end + test "it rejects Host header not matching Endpoint with port", %{conn: conn} do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("invalid.example.com:#{endpoint.port}") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.status == 400 + assert conn.halted == true + assert conn.resp_body == "Host header does not match this instance" + end + test "it rejects Host header not matching Endpoint port", %{conn: conn} do endpoint = URI.parse(Endpoint.url()) @@ -80,6 +93,18 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do assert conn.resp_body == "More than one Host header provided" end + test "it works for Host header without port", %{conn: conn} do + endpoint = URI.parse(Endpoint.url()) + + conn = + conn + |> set_host("#{endpoint.host}") + |> EnsureHostMatchesPlug.call(%{}) + + assert conn.halted == false + assert Map.get(conn.assigns, :valid_host_header, nil) + end + test "it works for Host header with port as 80", %{conn: conn} do endpoint = URI.parse(Endpoint.url()) From 6f415cf3fcdad8fc8d440a00c75e8388d99677e5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 12 May 2026 23:31:55 +0200 Subject: [PATCH 05/11] EnsureHostMatchesPlug: Remove match against default scheme port Checking against the default port of the Endpoint URL scheme is redundant as normal instances will have the combination https/443 by default created by pleroma.instance gen, Tor-only instances should have combination http/80 and local testing instances httt/XXXX. The default scheme port doesn't add anything usefull in these configs. --- .../web/plugs/ensure_host_matches_plug.ex | 17 ++++------- .../plugs/ensure_host_matches_plug_test.exs | 29 +------------------ 2 files changed, 7 insertions(+), 39 deletions(-) diff --git a/lib/pleroma/web/plugs/ensure_host_matches_plug.ex b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex index 87c4c0531..ab2129f9e 100644 --- a/lib/pleroma/web/plugs/ensure_host_matches_plug.ex +++ b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex @@ -17,7 +17,6 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do host_header = get_req_header(conn, "host") host_uri = URI.parse("//#{host_header}") instance_uri = URI.parse(Endpoint.url()) - instance_scheme_port = URI.default_port(instance_uri.scheme) case host_header do [host] -> @@ -26,7 +25,7 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do resp(conn, 400, "Host header not provided") |> halt() true -> - if host_matches?(host_uri, instance_uri, instance_scheme_port), + if host_matches?(host_uri, instance_uri), do: assign(conn, :valid_host_header, true), else: resp(conn, 400, "Host header does not match this instance") |> halt() end @@ -52,17 +51,13 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do # Host header did not provide port # Host header is scheme-less, URI.parse does not provide default port - defp host_matches?(%URI{host: req_host, port: nil}, %URI{host: instance_host}, _), + defp host_matches?(%URI{host: req_host, port: nil}, %URI{host: instance_host}), do: case_insensitive_compare(req_host, instance_host) - # Host header provided a port, reverse proxy configuration (port cannot match Endpoint port) - # Both port 80 and 443 are valid based on Endpoint configuration - defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host}, port), + # Host header provided a port + # Any port specified in the Endpoint url configuration is valid here + defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host, port: port}), do: case_insensitive_compare(req_host, instance_host) - # Host header provided port, configuration without reverse proxy (port matches Endpoint port) - defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host, port: port}, _), - do: case_insensitive_compare(req_host, instance_host) - - defp host_matches?(_, _, _), do: false + defp host_matches?(_, _), do: false end diff --git a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs index 67252e7a7..8ace74dfb 100644 --- a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs +++ b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do alias Pleroma.Web.Endpoint alias Pleroma.Web.Plugs.EnsureHostMatchesPlug - import Mock import Plug.Conn import Tesla.Mock @@ -105,33 +104,7 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do assert Map.get(conn.assigns, :valid_host_header, nil) end - test "it works for Host header with port as 80", %{conn: conn} do - endpoint = URI.parse(Endpoint.url()) - - conn = - conn - |> set_host("#{endpoint.host}:80") - |> EnsureHostMatchesPlug.call(%{}) - - assert conn.halted == false - assert Map.get(conn.assigns, :valid_host_header, nil) - end - - test "it works for Host header with port as 443", %{conn: conn} do - with_mock Pleroma.Web.Endpoint, url: fn -> "https://localhost:4001" end do - endpoint = URI.parse(Endpoint.url()) - - conn = - conn - |> set_host("#{endpoint.host}:443") - |> EnsureHostMatchesPlug.call(%{}) - - assert conn.halted == false - assert Map.get(conn.assigns, :valid_host_header, nil) - end - end - - test "it works for Host header with port as same as Endpoint (no reverse proxy config)", %{ + test "it works for Host header with port same as Endpoint", %{ conn: conn } do endpoint = URI.parse(Endpoint.url()) From 6c2d8209c9b3f86db6b89d48421b7a0d70a33d00 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 00:31:50 +0200 Subject: [PATCH 06/11] SignatureRetryWorker: require validated host header --- lib/pleroma/workers/signature_retry_worker.ex | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex index 2c4c097dd..28958faff 100644 --- a/lib/pleroma/workers/signature_retry_worker.ex +++ b/lib/pleroma/workers/signature_retry_worker.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Workers.SignatureRetryWorker do alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Federator + alias Pleroma.Web.Plugs.EnsureHostMatchesPlug alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug require Logger @@ -48,6 +49,7 @@ defmodule Pleroma.Workers.SignatureRetryWorker do {:ok, _public_key} <- Signature.refetch_public_key(conn_data), {:signature, true} <- {:signature, validate_signature(conn_data)}, {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, + {:host_header, true} <- {:host_header, validate_host_header(conn_data)}, {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do unless Instances.reachable?(params["actor"]) do domain = URI.parse(params["actor"]).host @@ -103,6 +105,16 @@ defmodule Pleroma.Workers.SignatureRetryWorker do end end + defp validate_host_header(conn_data) do + case EnsureHostMatchesPlug.call(conn_data, []) do + %Plug.Conn{assigns: %{valid_signature: true, valid_host_header: true}} -> + true + + _ -> + false + end + end + defp validate_same_actor(conn_data) do case MappedSignatureToIdentityPlug.call(conn_data, []) do %Plug.Conn{assigns: %{valid_signature: true}} -> @@ -170,6 +182,10 @@ defmodule Pleroma.Workers.SignatureRetryWorker do {:same_actor, false} -> {:cancel, :actor_signature_mismatch} + # Host header from request not for us + {:host_header, false} -> + {:cancel, :host_header_mismatch} + # Origin / URL validation failed somewhere possibly due to spoofing {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} @@ -234,6 +250,7 @@ defmodule Pleroma.Workers.SignatureRetryWorker do defp log_signature_retry_rejection({:cancel, reason}, context) when reason in [ :actor_signature_mismatch, + :host_header_mismatch, :invalid_signature, :invalid_signature_retry_metadata, :missing_signature_retry_metadata, From 95b15190dedab255c102db023af2d90eacf3a259 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 00:32:16 +0200 Subject: [PATCH 07/11] ActivityPubController: require validated host header --- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 2bfff6968..415aa4f68 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -303,7 +303,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end - def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do + def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, %{"nickname" => nickname} = params) do with {:recipient_exists, %User{} = recipient} <- {:recipient_exists, User.get_cached_by_nickname(nickname)}, {:sender_exists, {:ok, %User{} = actor}} <- @@ -342,7 +342,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end - def inbox(%{assigns: %{valid_signature: true}} = conn, params) do + def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, params) do Federator.incoming_ap_doc(params) json(conn, "ok") end From c19bdf38143fcbe3a91a3b6053acefb8925773ac Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 00:33:09 +0200 Subject: [PATCH 08/11] SignatureRetryWorker: add mismatched host test, fix tests --- .../workers/signature_retry_worker_test.exs | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs index 94dd5f6c1..7f1351f4a 100644 --- a/test/pleroma/workers/signature_retry_worker_test.exs +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -16,12 +16,13 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do alias Pleroma.Signature alias Pleroma.User alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.Endpoint alias Pleroma.Web.Federator alias Pleroma.Workers.SignatureRetryWorker defp signature_headers_for(%User{} = signer) do [ - {"host", "local.test"}, + {"host", "#{URI.parse(Endpoint.url()).host}"}, {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, {"digest", "SHA-256=fake-digest"}, {"content-type", "application/activity+json"}, @@ -245,6 +246,65 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do refute Activity.get_by_ap_id(create["id"]) end + test "cancels when the Host header does not match Endpoint" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/invalid-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/invalid-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + expect_signature_from(alice) + + headers = + [ + {"host", "invalid.example.com"}, + {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, + {"digest", "SHA-256=fake-digest"}, + {"content-type", "application/activity+json"}, + { + "signature", + "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + } + ] + + assert {:ok, oban_job} = Federator.incoming_failed_signature_ap_doc(%{ + method: "POST", + req_headers: headers, + request_path: "/inbox", + params: create, + query_string: "" + }) + + log = + capture_log([level: :warning], fn -> + assert {:cancel, :host_header_mismatch} = SignatureRetryWorker.perform(oban_job) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:host_header_mismatch" + assert log =~ "payload_actor=\"https://one.com/users/alice\"" + assert log =~ "signature_actor=\"https://one.com/users/alice\"" + assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\"" + assert log =~ "type=\"Create\"" + assert log =~ "request_path=\"/inbox\"" + + refute Activity.get_by_ap_id(create["id"]) + end + test "processes the activity after refetching a valid matching signature" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") @@ -309,11 +369,11 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do "content-type" => "application/activity+json", date: date, digest: digest, - host: "local.test" + host: "#{URI.parse(Endpoint.url()).host}" }) req_headers = [ - ["host", "local.test"], + ["host", "#{URI.parse(Endpoint.url()).host}"], ["date", date], ["digest", digest], ["content-type", "application/activity+json"], From 95eef879d76df69e4d113f79afa9fbd2188aa5b0 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 00:40:53 +0200 Subject: [PATCH 09/11] ActivityPubController: add mismatched host test --- .../activity_pub_controller_test.exs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) 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 3988c3912..64f79dab5 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -950,6 +950,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do refute Activity.get_by_ap_id(data["id"]) end + test "does not process post with Host header not for us", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + object_id = "https://one.com/objects/inbox-forged-note" + + data = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/inbox-forged-create", + "context" => "https://one.com/contexts/inbox-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "context" => "https://one.com/contexts/inbox-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + expect_signature_retry_from(alice) + + conn = %{conn | req_headers: [{"host", "invalid.example.com"}]} + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "Host header does not match this instance" == conn.resp_body + assert 400 == conn.status + assert true == conn.halted + + refute Activity.get_by_ap_id(data["id"]) + refute Object.get_by_ap_id(object_id) + end + test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() From 2b3ac2d7fee91cdcbe53ca53760559def250b7d7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 00:44:33 +0200 Subject: [PATCH 10/11] lint --- .../web/activity_pub/activity_pub_controller.ex | 7 ++++++- .../workers/signature_retry_worker_test.exs | 15 ++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 415aa4f68..3d1875599 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -303,7 +303,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end - def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, %{"nickname" => nickname} = params) do + def inbox( + %{ + assigns: %{valid_signature: true, valid_host_header: true} + } = conn, + %{"nickname" => nickname} = params + ) do with {:recipient_exists, %User{} = recipient} <- {:recipient_exists, User.get_cached_by_nickname(nickname)}, {:sender_exists, {:ok, %User{} = actor}} <- diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs index 7f1351f4a..3806ecac9 100644 --- a/test/pleroma/workers/signature_retry_worker_test.exs +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -281,13 +281,14 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do } ] - assert {:ok, oban_job} = Federator.incoming_failed_signature_ap_doc(%{ - method: "POST", - req_headers: headers, - request_path: "/inbox", - params: create, - query_string: "" - }) + assert {:ok, oban_job} = + Federator.incoming_failed_signature_ap_doc(%{ + method: "POST", + req_headers: headers, + request_path: "/inbox", + params: create, + query_string: "" + }) log = capture_log([level: :warning], fn -> From 4810d2536e2fd366364c1ec39d88b21265166f88 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 13 May 2026 12:10:10 +0200 Subject: [PATCH 11/11] ActivityPubController: Use valid signatures in Host header test --- .../activity_pub/activity_pub_controller_test.exs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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 64f79dab5..68e39b3a0 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -974,16 +974,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } } - expect_signature_retry_from(alice) - - conn = %{conn | req_headers: [{"host", "invalid.example.com"}]} - + # Plug will complain when replacing raw host header with put_req_header. + # The Plug way is updating conn.host, but that isn't the raw header + # and that isn't used in the EnsureHostMatchesPlug, because it doesn't include the port. conn = conn - |> assign(:valid_signature, false) + |> assign_valid_signature_for_actor(alice) + |> delete_req_header("host") |> put_req_header("content-type", "application/activity+json") - |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") - |> post("/inbox", data) + + conn = %{conn | req_headers: conn.req_headers ++ [{"host", "invalid.example.com"}]} + conn = post(conn, "/inbox", data) assert "Host header does not match this instance" == conn.resp_body assert 400 == conn.status