Validate WebFinger nicknames against actors

This commit is contained in:
Lain Soykaf 2026-05-03 18:02:59 +04:00
commit 621d86a31d
No known key found for this signature in database
2 changed files with 161 additions and 40 deletions

View file

@ -1677,44 +1677,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
show_birthday = !!birthday show_birthday = !!birthday
# if WebFinger request was already done, we probably have acct, otherwise with {:ok, nickname} <- nickname_from_actor(data, additional) do
# we request WebFinger here {:ok,
nickname = additional[:nickname_from_acct] || generate_nickname(data) %{
ap_id: data["id"],
uri: get_actor_url(data["url"]),
banner: normalize_image(data["image"]),
fields: fields,
emoji: emojis,
is_locked: is_locked,
is_discoverable: is_discoverable,
invisible: invisible,
avatar: normalize_image(data["icon"]),
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
featured_address: featured_address,
bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: normalize_also_known_as(data["alsoKnownAs"]),
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
accepts_chat_messages: accepts_chat_messages,
birthday: birthday,
show_birthday: show_birthday,
pinned_objects: pinned_objects,
nickname: nickname
}}
end
end
%{ defp nickname_from_actor(data, additional) do
ap_id: data["id"], generated = generated_nickname(data)
uri: get_actor_url(data["url"]),
banner: normalize_image(data["image"]), case additional[:nickname_from_acct] do
fields: fields, ^generated when is_binary(generated) ->
emoji: emojis, {:ok, generated}
is_locked: is_locked,
is_discoverable: is_discoverable, acct when is_binary(acct) ->
invisible: invisible, with ^acct <- webfinger_nickname(data) do
avatar: normalize_image(data["icon"]), {:ok, acct}
name: data["name"], else
follower_address: data["followers"], _ -> {:error, {:webfinger_actor_mismatch, acct, data["id"]}}
following_address: data["following"], end
featured_address: featured_address,
bio: data["summary"] || "", _ ->
actor_type: actor_type, {:ok, generate_nickname(data)}
also_known_as: normalize_also_known_as(data["alsoKnownAs"]), end
public_key: public_key, end
inbox: data["inbox"],
shared_inbox: shared_inbox, defp generated_nickname(%{"preferredUsername" => username, "id" => ap_id})
accepts_chat_messages: accepts_chat_messages, when is_binary(username) and is_binary(ap_id) do
birthday: birthday, case URI.parse(ap_id) do
show_birthday: show_birthday, %URI{host: host} when is_binary(host) -> "#{username}@#{host}"
pinned_objects: pinned_objects, _ -> nil
nickname: nickname end
} end
defp generated_nickname(_), do: nil
defp webfinger_nickname(data) do
with generated when is_binary(generated) <- generated_nickname(data),
{:ok, %{"subject" => "acct:" <> acct, "ap_id" => ap_id}} <- WebFinger.finger(generated),
true <- ap_id == data["id"] do
acct
end
end end
defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
generated = "#{username}@#{URI.parse(data["id"]).host}" generated = generated_nickname(data)
if Config.get([WebFinger, :update_nickname_on_user_fetch]) do if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
case WebFinger.finger(generated) do case webfinger_nickname(data) do
{:ok, %{"subject" => "acct:" <> acct}} -> acct acct when is_binary(acct) -> acct
_ -> generated _ -> generated
end end
else else
@ -1794,9 +1830,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp collection_private(_data), do: {:ok, true} defp collection_private(_data), do: {:ok, true}
def user_data_from_user_object(data, additional \\ []) do def user_data_from_user_object(data, additional \\ []) do
with {:ok, data} <- MRF.filter(data) do with {:ok, data} <- MRF.filter(data),
{:ok, object_to_user_data(data, additional)} {:ok, data} <- object_to_user_data(data, additional) do
{:ok, data}
else else
{:error, _} = e -> e
e -> {:error, e} e -> {:error, e}
end end
end end

View file

@ -876,17 +876,17 @@ defmodule Pleroma.UserTest do
describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true) setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
test "for mastodon" do test "fetches a mastodon split-domain nickname" do
ap_id = "a@mastodon.example" nickname = "a@mastodon.example"
{:ok, fetched_user} = User.get_or_fetch(ap_id) {:ok, fetched_user} = User.get_or_fetch(nickname)
assert fetched_user.ap_id == "https://sub.mastodon.example/users/a" assert fetched_user.ap_id == "https://sub.mastodon.example/users/a"
assert fetched_user.nickname == "a@mastodon.example" assert fetched_user.nickname == "a@mastodon.example"
end end
test "for pleroma" do test "fetches a pleroma split-domain nickname" do
ap_id = "a@pleroma.example" nickname = "a@pleroma.example"
{:ok, fetched_user} = User.get_or_fetch(ap_id) {:ok, fetched_user} = User.get_or_fetch(nickname)
assert fetched_user.ap_id == "https://sub.pleroma.example/users/a" assert fetched_user.ap_id == "https://sub.pleroma.example/users/a"
assert fetched_user.nickname == "a@pleroma.example" assert fetched_user.nickname == "a@pleroma.example"
@ -936,6 +936,89 @@ defmodule Pleroma.UserTest do
assert fetched_user == "not found nonexistent" assert fetched_user == "not found nonexistent"
end end
test "does not rename an existing remote actor from rogue WebFinger data" do
clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
actor_id = "https://legit-actor.example/users/alice"
Tesla.Mock.mock(fn
%{url: "https://evil-webfinger.example/.well-known/host-meta"} ->
{:ok, %Tesla.Env{status: 404}}
%{
url:
"https://evil-webfinger.example/.well-known/webfinger?resource=acct:claimed@evil-webfinger.example"
} ->
Tesla.Mock.json(%{
"subject" => "acct:claimed@evil-webfinger.example",
"links" => [
%{
"rel" => "self",
"type" => "application/activity+json",
"href" => actor_id
}
]
})
%{url: ^actor_id} ->
{:ok,
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
Jason.encode!(%{
"id" => actor_id,
"type" => "Person",
"preferredUsername" => "alice",
"name" => "Alice",
"summary" => "",
"inbox" => "https://legit-actor.example/users/alice/inbox",
"outbox" => "https://legit-actor.example/users/alice/outbox",
"followers" => "https://legit-actor.example/users/alice/followers",
"following" => "https://legit-actor.example/users/alice/following"
})
}}
%{url: "https://legit-actor.example/.well-known/host-meta"} ->
{:ok, %Tesla.Env{status: 404}}
%{
url:
"https://legit-actor.example/.well-known/webfinger?resource=acct:alice@legit-actor.example"
} ->
Tesla.Mock.json(%{
"subject" => "acct:alice@legit-actor.example",
"links" => [
%{
"rel" => "self",
"type" => "application/activity+json",
"href" => actor_id
}
]
})
end)
assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
refute User.get_by_ap_id(actor_id)
refute User.get_by_nickname("claimed@evil-webfinger.example")
orig_user =
insert(:user,
local: false,
nickname: "alice@legit-actor.example",
ap_id: actor_id
)
assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
assert {:error, _} = User.get_or_fetch_by_nickname("claimed@evil-webfinger.example")
assert User.get_by_id(orig_user.id).nickname == "alice@legit-actor.example"
refute User.get_by_nickname("claimed@evil-webfinger.example")
end
test "updates an existing user, if stale" do test "updates an existing user, if stale" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)