From 3e2573f1c4688a227dae2a8fa96141c0a65cffbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 15 Dec 2025 16:52:45 +0100 Subject: [PATCH 1/6] Fix WebFinger for split-domain set ups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/web_finger.ex | 34 +++++++++++++++++++------- test/pleroma/web/web_finger_test.exs | 36 ++++++++++++++++++---------- test/support/http_request_mock.ex | 3 ++- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex index a53d58caa..0ac467947 100644 --- a/lib/pleroma/web/web_finger.ex +++ b/lib/pleroma/web/web_finger.ex @@ -194,8 +194,8 @@ defmodule Pleroma.Web.WebFinger do defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain} - @spec finger(String.t()) :: {:ok, map()} | {:error, any()} - def finger(account) do + @spec finger(String.t(), boolean()) :: {:ok, map()} | {:error, any()} + def finger(account, follow_redirects \\ true) do account = String.trim_leading(account, "@") domain = @@ -229,8 +229,15 @@ defmodule Pleroma.Web.WebFinger do {:error, {:content_type, nil}} end |> case do - {:ok, data} -> validate_webfinger(address, data) - error -> error + {:ok, data} -> + if follow_redirects do + validate_webfinger(address, data) + else + {:ok, data} + end + + error -> + error end else error -> @@ -241,10 +248,8 @@ defmodule Pleroma.Web.WebFinger do defp validate_webfinger(request_url, %{"subject" => "acct:" <> acct = subject} = data) do with [_name, acct_host] <- String.split(acct, "@"), - {_, url} <- {:address, get_address_from_domain(acct_host, subject)}, - %URI{host: request_host} <- URI.parse(request_url), - %URI{host: acct_host} <- URI.parse(url), - {_, true} <- {:hosts_match, acct_host == request_host} do + {_, resolved_url} <- {:address, get_address_from_domain(acct_host, subject)}, + {_, true} <- {:url_match, resolved_webfinger_matches?(request_url, resolved_url, data)} do {:ok, data} else _ -> {:error, {:webfinger_invalid, request_url, data}} @@ -252,4 +257,17 @@ defmodule Pleroma.Web.WebFinger do end defp validate_webfinger(url, data), do: {:error, {:webfinger_invalid, url, data}} + + defp resolved_webfinger_matches?(request_url, resolved_url, _data) + when request_url == resolved_url do + true + end + + defp resolved_webfinger_matches?(_request_url, _resolved_url, %{"subject" => "acct:" <> acct}) do + with {:ok, %{"subject" => "acct:" <> new_acct}} <- finger(acct, false) do + acct == new_acct + else + _ -> false + end + end end diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index 923074ed5..4f4890778 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -93,7 +93,7 @@ defmodule Pleroma.Web.WebFingerTest do {:ok, _data} = WebFinger.finger(user) end - test "it work for AP-only user" do + test "it works for AP-only user" do user = "kpherox@mstdn.jp" {:ok, data} = WebFinger.finger(user) @@ -224,24 +224,36 @@ defmodule Pleroma.Web.WebFingerTest do status: 200, body: File.read!("test/fixtures/tesla_mock/gleasonator.com_host_meta") }} + + %{url: "https://whitehouse.gov/.well-known/webfinger?resource=acct:trump@whitehouse.gov"} -> + {:ok, %Tesla.Env{status: 404}} end) {:error, _data} = WebFinger.finger("alex@gleasonator.com") end - end - test "prevents forgeries" do - Tesla.Mock.mock(fn - %{url: "https://fba.ryona.agency/.well-known/webfinger?resource=acct:graf@fba.ryona.agency"} -> - fake_webfinger = - File.read!("test/fixtures/webfinger/graf-imposter-webfinger.json") |> Jason.decode!() + test "prevents forgeries" do + Tesla.Mock.mock(fn + %{url: "https://fba.ryona.agency/.well-known/webfinger?resource=acct:graf@fba.ryona.agency"} -> + fake_webfinger = + File.read!("test/fixtures/webfinger/graf-imposter-webfinger.json") |> Jason.decode!() - Tesla.Mock.json(fake_webfinger) + Tesla.Mock.json(fake_webfinger) - %{url: "https://fba.ryona.agency/.well-known/host-meta"} -> - {:ok, %Tesla.Env{status: 404}} - end) + %{url: url} + when url in [ + "https://poa.st/.well-known/webfinger?resource=acct:graf@poa.st", + "https://fba.ryona.agency/.well-known/host-meta" + ] -> + {:ok, %Tesla.Env{status: 404}} + end) - assert {:error, _} = WebFinger.finger("graf@fba.ryona.agency") + assert {:error, _} = WebFinger.finger("graf@fba.ryona.agency") + end + + test "works for correctly set up split-domain instances" do + {:ok, _data} = WebFinger.finger("a@mastodon.example") + {:ok, _data} = WebFinger.finger("a@sub.mastodon.example") + end end end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index f8d11e602..51219e63f 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1229,7 +1229,8 @@ defmodule HttpRequestMock do {:ok, %Tesla.Env{status: 404, body: ""}} end - def get("https://mstdn.jp/.well-known/webfinger?resource=acct:kpherox@mstdn.jp", _, _, _) do + def get("https://mstdn.jp/.well-known/webfinger?resource=acct:" <> acct, _, _, _) + when acct in ["kpherox@mstdn.jp", "kPherox@mstdn.jp"] do {:ok, %Tesla.Env{ status: 200, From e5be1d04d6fadf5a7c92ec86c5c97efd7ea09501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 15 Dec 2025 16:59:23 +0100 Subject: [PATCH 2/6] Update tests, make the mastodon subdomain example not have the /.well-known/host-meta redirect, as the docs don't include it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- test/pleroma/web/web_finger_test.exs | 18 +++++++++---- test/support/http_request_mock.ex | 40 +++++++++++++++++++--------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index 4f4890778..7b4b2d523 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -234,7 +234,10 @@ defmodule Pleroma.Web.WebFingerTest do test "prevents forgeries" do Tesla.Mock.mock(fn - %{url: "https://fba.ryona.agency/.well-known/webfinger?resource=acct:graf@fba.ryona.agency"} -> + %{ + url: + "https://fba.ryona.agency/.well-known/webfinger?resource=acct:graf@fba.ryona.agency" + } -> fake_webfinger = File.read!("test/fixtures/webfinger/graf-imposter-webfinger.json") |> Jason.decode!() @@ -242,16 +245,21 @@ defmodule Pleroma.Web.WebFingerTest do %{url: url} when url in [ - "https://poa.st/.well-known/webfinger?resource=acct:graf@poa.st", - "https://fba.ryona.agency/.well-known/host-meta" - ] -> + "https://poa.st/.well-known/webfinger?resource=acct:graf@poa.st", + "https://fba.ryona.agency/.well-known/host-meta" + ] -> {:ok, %Tesla.Env{status: 404}} end) assert {:error, _} = WebFinger.finger("graf@fba.ryona.agency") end - test "works for correctly set up split-domain instances" do + test "works for correctly set up split-domain instances implementing host-meta redirect" do + {:ok, _data} = WebFinger.finger("a@pleroma.example") + {:ok, _data} = WebFinger.finger("a@sub.pleroma.example") + end + + test "works for correctly set up split-domain instances without host-meta redirect" do {:ok, _data} = WebFinger.finger("a@mastodon.example") {:ok, _data} = WebFinger.finger("a@sub.mastodon.example") end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 51219e63f..82affe7ac 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1527,14 +1527,6 @@ defmodule HttpRequestMock do }} end - def get("https://mastodon.example/.well-known/host-meta", _, _, _) do - {:ok, - %Tesla.Env{ - status: 302, - headers: [{"location", "https://sub.mastodon.example/.well-known/host-meta"}] - }} - end - def get("https://sub.mastodon.example/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ @@ -1547,11 +1539,15 @@ defmodule HttpRequestMock do end def get( - "https://sub.mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example", + url, _, _, _ - ) do + ) + when url in [ + "https://sub.mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example", + "https://sub.mastodon.example/.well-known/webfinger?resource=acct:a@sub.mastodon.example" + ] do {:ok, %Tesla.Env{ status: 200, @@ -1565,6 +1561,22 @@ defmodule HttpRequestMock do }} end + def get( + "https://mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example", + _, + _, + _ + ) do + {:ok, + %Tesla.Env{ + status: 302, + headers: [ + {"location", + "https://sub.mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example"} + ] + }} + end + def get("https://sub.mastodon.example/users/a", _, _, _) do {:ok, %Tesla.Env{ @@ -1610,11 +1622,15 @@ defmodule HttpRequestMock do end def get( - "https://sub.pleroma.example/.well-known/webfinger?resource=acct:a@pleroma.example", + url, _, _, _ - ) do + ) + when url in [ + "https://sub.pleroma.example/.well-known/webfinger?resource=acct:a@pleroma.example", + "https://sub.pleroma.example/.well-known/webfinger?resource=acct:a@sub.pleroma.example" + ] do {:ok, %Tesla.Env{ status: 200, From cacb2ce377e0ff24cfb0e6570bc1b75ff1b027e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 15 Dec 2025 17:25:57 +0100 Subject: [PATCH 3/6] Update changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/webfinger-actual-fix.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/webfinger-actual-fix.fix diff --git a/changelog.d/webfinger-actual-fix.fix b/changelog.d/webfinger-actual-fix.fix new file mode 100644 index 000000000..6aaf89d68 --- /dev/null +++ b/changelog.d/webfinger-actual-fix.fix @@ -0,0 +1 @@ +Fix WebFinger for split-domain setups From 45af48520bf605dc1fa2e28a53a327a535f8acd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 15 Dec 2025 18:10:00 +0100 Subject: [PATCH 4/6] this shouldn't be available outside the module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/web_finger.ex | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex index 0ac467947..909fe9781 100644 --- a/lib/pleroma/web/web_finger.ex +++ b/lib/pleroma/web/web_finger.ex @@ -194,8 +194,10 @@ defmodule Pleroma.Web.WebFinger do defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain} - @spec finger(String.t(), boolean()) :: {:ok, map()} | {:error, any()} - def finger(account, follow_redirects \\ true) do + @spec finger(String.t()) :: {:ok, map()} | {:error, any()} + def finger(account), do: do_finger(account, true) + + defp do_finger(account, follow_redirects) do account = String.trim_leading(account, "@") domain = @@ -264,7 +266,7 @@ defmodule Pleroma.Web.WebFinger do end defp resolved_webfinger_matches?(_request_url, _resolved_url, %{"subject" => "acct:" <> acct}) do - with {:ok, %{"subject" => "acct:" <> new_acct}} <- finger(acct, false) do + with {:ok, %{"subject" => "acct:" <> new_acct}} <- do_finger(acct, false) do acct == new_acct else _ -> false From f70d1a436b343f010b3b8d638151d746b889c01b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 21 Dec 2025 17:46:20 +0400 Subject: [PATCH 5/6] WebFingerTest: Add test for more webfinger spoofing. --- test/pleroma/web/web_finger_test.exs | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index 7b4b2d523..eb03c736e 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -254,6 +254,46 @@ defmodule Pleroma.Web.WebFingerTest do assert {:error, _} = WebFinger.finger("graf@fba.ryona.agency") end + test "prevents forgeries even when the spoofed subject exists on the target domain" do + Tesla.Mock.mock(fn + %{url: url} + when url in [ + "https://attacker.example/.well-known/host-meta", + "https://victim.example/.well-known/host-meta" + ] -> + {:ok, %Tesla.Env{status: 404}} + + %{ + url: + "https://attacker.example/.well-known/webfinger?resource=acct:alice@attacker.example" + } -> + Tesla.Mock.json(%{ + "subject" => "acct:alice@victim.example", + "links" => [ + %{ + "rel" => "self", + "type" => "application/activity+json", + "href" => "https://attacker.example/users/alice" + } + ] + }) + + %{url: "https://victim.example/.well-known/webfinger?resource=acct:alice@victim.example"} -> + Tesla.Mock.json(%{ + "subject" => "acct:alice@victim.example", + "links" => [ + %{ + "rel" => "self", + "type" => "application/activity+json", + "href" => "https://victim.example/users/alice" + } + ] + }) + end) + + assert {:error, _} = WebFinger.finger("alice@attacker.example") + end + test "works for correctly set up split-domain instances implementing host-meta redirect" do {:ok, _data} = WebFinger.finger("a@pleroma.example") {:ok, _data} = WebFinger.finger("a@sub.pleroma.example") From e9d972463793c1e3bba78ee4607fd00c271d468b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 21 Dec 2025 17:46:39 +0400 Subject: [PATCH 6/6] WebFinger: Tighten the requirements. --- lib/pleroma/web/web_finger.ex | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex index 909fe9781..62a2715f5 100644 --- a/lib/pleroma/web/web_finger.ex +++ b/lib/pleroma/web/web_finger.ex @@ -265,11 +265,23 @@ defmodule Pleroma.Web.WebFinger do true end - defp resolved_webfinger_matches?(_request_url, _resolved_url, %{"subject" => "acct:" <> acct}) do - with {:ok, %{"subject" => "acct:" <> new_acct}} <- do_finger(acct, false) do - acct == new_acct + defp resolved_webfinger_matches?( + _request_url, + _resolved_url, + %{"subject" => "acct:" <> acct} = data + ) do + with {:ok, %{"subject" => "acct:" <> new_acct} = new_data} <- do_finger(acct, false), + true <- acct == new_acct, + true <- webfinger_data_matches?(data, new_data) do + true else _ -> false end end + + defp webfinger_data_matches?(%{"ap_id" => ap_id}, %{"ap_id" => ap_id}) when ap_id != "" do + true + end + + defp webfinger_data_matches?(_data, _new_data), do: false end