commit: d19b992417fe55729e331c77fbce9e30e8923844
parent 985a0a28c708f7abb5b6db39db8baeb2f4311333
Author: lain <lain@soykaf.club>
Date: Mon, 22 Dec 2025 07:38:55 +0000
Merge branch 'webfinger-actual-fix' into 'develop'
Fix WebFinger for split-domain setups
See merge request pleroma/pleroma!4405
Diffstat:
4 files changed, 142 insertions(+), 32 deletions(-)
diff --git a/changelog.d/webfinger-actual-fix.fix b/changelog.d/webfinger-actual-fix.fix
@@ -0,0 +1 @@
+Fix WebFinger for split-domain setups
diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex
@@ -195,7 +195,9 @@ 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
+ def finger(account), do: do_finger(account, true)
+
+ defp do_finger(account, follow_redirects) do
account = String.trim_leading(account, "@")
domain =
@@ -229,8 +231,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 +250,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 +259,29 @@ 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} = 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
diff --git 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,84 @@ 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")
+ 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}}
- assert {:error, _} = WebFinger.finger("graf@fba.ryona.agency")
+ %{
+ 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")
+ 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
end
end
diff --git 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,
@@ -1526,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{
@@ -1546,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,
@@ -1564,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{
@@ -1609,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,