Merge branch 'develop' into gitlab-mr-iid-4426

This commit is contained in:
lain 2026-05-14 06:56:57 +00:00
commit c7c453ca21
34 changed files with 1574 additions and 115 deletions

View file

@ -104,7 +104,7 @@ defmodule Pleroma.Signature do
|> put_req_header("(request-target)", request_target)
|> put_req_header("@request-target", request_target)
@http_signatures_impl.validate_conn(conn)
@http_signatures_impl.validate_conn(conn) == true
end
@spec validate_signature(Plug.Conn.t()) :: boolean()

View file

@ -4,6 +4,7 @@
defmodule Pleroma.User.Search do
alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType
alias Pleroma.Instances.Instance
alias Pleroma.Pagination
alias Pleroma.User
@ -88,12 +89,13 @@ defmodule Pleroma.User.Search do
|> filter_invisible_users()
|> filter_internal_users()
|> filter_blocked_domains(for_user)
|> filter_unreachable_users()
|> fts_search(query_string)
|> select_top_users(top_user_ids)
|> trigram_rank(query_string)
|> boost_search_rank(for_user, top_user_ids)
|> subquery()
|> order_by(desc: :search_rank)
|> order_by_search_rank(for_user)
|> maybe_restrict_local(for_user)
|> maybe_restrict_accepting_chat_messages(capabilities)
|> filter_deactivated_users()
@ -196,6 +198,14 @@ defmodule Pleroma.User.Search do
defp filter_blocked_domains(query, _), do: query
defp filter_unreachable_users(query) do
from(u in query,
left_join: i in Instance,
on: i.host == fragment("substring(? from '.*://([^/]*)')", u.ap_id),
where: is_nil(i.unreachable_since)
)
end
defp maybe_resolve(true, user, query) do
case {limit(), user} do
{:all, _} -> :noop
@ -236,6 +246,16 @@ defmodule Pleroma.User.Search do
from(u in subquery(query),
select_merge: %{
search_type:
fragment(
"""
CASE WHEN (?) THEN 2
WHEN (?) THEN 1
ELSE 0 END
""",
u.id in ^top_user_ids,
u.id in ^friends_ids or u.id in ^followers_ids
),
search_rank:
fragment(
"""
@ -261,6 +281,14 @@ defmodule Pleroma.User.Search do
defp boost_search_rank(query, _for_user, top_user_ids) do
from(u in subquery(query),
select_merge: %{
search_type:
fragment(
"""
CASE WHEN (?) THEN 2
ELSE 0 END
""",
u.id in ^top_user_ids
),
search_rank:
fragment(
"""
@ -273,4 +301,22 @@ defmodule Pleroma.User.Search do
}
)
end
defp order_by_search_rank(query, %User{}) do
order_by(
query,
[u],
desc: u.search_type,
desc_nulls_last:
fragment(
"CASE WHEN ? = 1 THEN COALESCE(?, ?) ELSE NULL END",
u.search_type,
u.last_status_at,
u.last_active_at
),
desc: u.search_rank
)
end
defp order_by_search_rank(query, _), do: order_by(query, desc: :search_rank)
end

View file

@ -303,7 +303,12 @@ 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 +347,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

View file

@ -430,6 +430,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end)
end
defp reject_third_party_report(%User{local: false}, %User{local: false} = account) do
{:reject, "[Transmogrifier] third-party report: #{account.ap_id}"}
end
defp reject_third_party_report(_, _), do: :ok
def handle_incoming(data, options \\ []) do
data
|> fix_recursive(&strip_internal_fields/1)
@ -444,9 +450,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
) do
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
objects <- List.wrap(objects),
%User{} = actor <- User.get_cached_by_ap_id(actor),
# Reduce the object list to find the reported user.
%User{} = account <- get_reported(objects),
:ok <- reject_third_party_report(actor, account),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
%{

View file

@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# 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())
case host_header do
[host] ->
cond do
host == "" ->
resp(conn, 400, "Host header not provided") |> halt()
true ->
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
[_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
# 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)
defp host_matches?(_, _), do: false
end

View file

@ -4,7 +4,7 @@
defmodule Pleroma.Web.Plugs.RemoteIp do
@moduledoc """
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
This is a shim to call [`RemoteIp`](https://hex.pm/packages/remote_ip) but with runtime configuration.
"""
alias Pleroma.Config
@ -17,15 +17,29 @@ defmodule Pleroma.Web.Plugs.RemoteIp do
def call(%{remote_ip: original_remote_ip} = conn, _) do
if Config.get([__MODULE__, :enabled]) do
%{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts())
new_remote_ip = remote_ip(conn) || original_remote_ip
conn = %{conn | remote_ip: new_remote_ip}
assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
else
conn
end
end
defp remote_ip(conn) do
opts = remote_ip_opts()
# Do not use RemoteIp.from/2 here: upstream remote_ip always applies its
# built-in reserved ranges. Pleroma keeps :reserved configurable, so reuse
# only the header parsing and apply Pleroma's own block classification.
conn.req_headers
|> RemoteIp.Headers.take(opts[:headers])
|> RemoteIp.Headers.parse()
|> Enum.reverse()
|> Enum.find(&client?(&1, opts))
end
defp remote_ip_opts do
headers = Config.get([__MODULE__, :headers], []) |> MapSet.new()
reserved = Config.get([__MODULE__, :reserved], [])
proxies =
@ -33,6 +47,26 @@ defmodule Pleroma.Web.Plugs.RemoteIp do
|> Enum.concat(reserved)
|> Enum.map(&InetHelper.parse_cidr/1)
{headers, proxies}
clients =
Config.get([__MODULE__, :clients], [])
|> Enum.map(&InetHelper.parse_cidr/1)
[
headers: Config.get([__MODULE__, :headers], []),
clients: clients,
proxies: proxies
]
end
defp client?(ip, opts) do
client_ip?(ip, opts[:clients]) || !proxy_ip?(ip, opts[:proxies])
end
defp client_ip?(ip, clients) do
Enum.any?(clients, &InetCidr.contains?(&1, ip))
end
defp proxy_ip?(ip, proxies) do
Enum.any?(proxies, &InetCidr.contains?(&1, ip))
end
end

View file

@ -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

View file

@ -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,