fix(federation): reject actors that point at other hosts

A remote actor could set its inbox or shared inbox to a third-party
host, so our signed deliveries went there. It could also claim
another actor's followers collection, which the visibility checks
trust.

Validate fetched actors and incoming actor Updates: inbox,
endpoints.sharedInbox, followers, following, featured and the
publicKey id must be on the same host as the actor id.

Based on Akkoma f07eb4cb5. The Bridgy fixture gets the key id that
current Bridgy Fed serves.
This commit is contained in:
Lain Soykaf 2026-10-27 21:37:20 +04:00
commit 695dd340de
No known key found for this signature in database
9 changed files with 201 additions and 8 deletions

View file

@ -112,4 +112,14 @@ defmodule Pleroma.Object.Containment do
do: contain_origin(id, object)
def contain_child(_), do: :ok
@doc "Checks that two URIs are on the same host. URIs without a host never match."
def same_origin(id, other_id) when is_binary(id) and is_binary(other_id) do
case {URI.parse(id), URI.parse(other_id)} do
{%URI{host: host}, %URI{host: host}} when is_binary(host) and host != "" -> :ok
_ -> :error
end
end
def same_origin(_id, _other_id), do: :error
end

View file

@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger
@ -1924,6 +1925,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def user_data_from_user_object(data, additional \\ []) do
with {:ok, data} <- MRF.filter(data),
{:ok, data, _} <- UserValidator.validate(data, []),
{:ok, data} <- object_to_user_data(data, additional) do
{:ok, data}
else

View file

@ -11,7 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
import Pleroma.Constants, only: [activity_types: 0, object_types: 0]
import Pleroma.Constants, only: [activity_types: 0, actor_types: 0, object_types: 0]
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
@ -37,6 +37,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator
@impl true
def validate(object, meta)
@ -226,7 +227,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
def validate(%{"type" => type} = object, meta) when type == "Update" do
with {:ok, object} <-
with :ok <- validate_updated_actor(object, meta),
{:ok, object} <-
object
|> UpdateValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do
@ -247,6 +249,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(o, m), do: {:error, {:validator_not_set, {o, m}}}
defp validate_updated_actor(%{"object" => %{"type" => type} = actor}, meta)
when type in actor_types() do
with {_, false} <- {:local, Access.get(meta, :local, false)},
{:ok, _, _} <- UserValidator.validate(actor, meta) do
:ok
else
{:local, true} -> :ok
error -> error
end
end
defp validate_updated_actor(_, _), do: :ok
def cast_and_apply_and_stringify_with_history(object) do
do_separate_with_history(object, fn object ->
with {:ok, object_data} <- cast_and_apply(object),

View file

@ -0,0 +1,52 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do
@moduledoc """
Checks that a remote actor document only points at its own host.
We deliver to the inbox and shared inbox, use the collections for
addressing and visibility checks, and map signature key ids back to
the actor. If an actor could point any of these at another host, it
could redirect our signed deliveries or claim another actor's
collections. Optional fields are only checked when present.
Based on Akkoma's UserValidator.
"""
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
alias Pleroma.Object.Containment
@collections ["inbox", "followers", "following", "featured"]
@impl true
def validate(%{"id" => id} = data, meta) when is_binary(id) do
fields =
Enum.map(@collections, &{&1, data[&1]}) ++
[
{"sharedInbox", get_in_map(data, ["endpoints", "sharedInbox"])},
{"publicKey", get_in_map(data, ["publicKey", "id"])}
]
case Enum.find(fields, fn {_, value} -> not same_origin?(id, value) end) do
nil -> {:ok, data, meta}
{field, _} -> {:error, "#{field} is not on the same host as the actor"}
end
end
def validate(_data, _meta), do: {:error, "Actor has no id"}
defp same_origin?(_id, nil), do: true
defp same_origin?(id, %{"id" => value}), do: same_origin?(id, value)
defp same_origin?(id, value), do: Containment.same_origin(id, value) == :ok
defp get_in_map(data, [key | rest]) do
case data do
%{^key => value} when rest == [] -> value
%{^key => value} -> get_in_map(value, rest)
_ -> nil
end
end
end