From 74da3e01f7f0b27fcbbec313db363118c51388a8 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 5 Mar 2026 16:36:22 +0400 Subject: [PATCH] Harden ParadeDB search result safety Re-apply public Create visibility checks when resolving backend hits, improve UUID/FlakeId id parsing, and add tests for poisoned hits and UUID-string ids. Also tighten the docker integration test with localhost-only binding and config restoration. --- lib/pleroma/search/paradedb.ex | 41 ++++++++++-- .../search/paradedb_integration_test.exs | 13 +++- test/pleroma/search/paradedb_test.exs | 63 +++++++++++++++++++ 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/search/paradedb.ex b/lib/pleroma/search/paradedb.ex index e5d3018d7..01cbc323f 100644 --- a/lib/pleroma/search/paradedb.ex +++ b/lib/pleroma/search/paradedb.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Search.ParadeDB do @behaviour Pleroma.Search.SearchBackend require Logger + require Pleroma.Constants import Ecto.Query @@ -153,6 +154,7 @@ defmodule Pleroma.Search.ParadeDB do from(a in Activity, where: a.id in ^dumped_ids) |> Activity.with_preloaded_object() |> Activity.restrict_deactivated_users() + |> maybe_restrict_public(user) |> maybe_restrict_local(user) |> maybe_restrict_author(author) |> maybe_restrict_blocked(user) @@ -168,6 +170,25 @@ defmodule Pleroma.Search.ParadeDB do |> maybe_fetch(user, query) end + defp maybe_restrict_public(query, %Pleroma.User{}) do + intended_recipients = [ + Pleroma.Constants.as_public(), + Pleroma.Web.ActivityPub.Utils.as_local_public() + ] + + from(a in query, + where: fragment("?->>'type' = 'Create'", a.data), + where: fragment("? && ?", ^intended_recipients, a.recipients) + ) + end + + defp maybe_restrict_public(query, _user) do + from(a in query, + where: fragment("?->>'type' = 'Create'", a.data), + where: ^Pleroma.Constants.as_public() in a.recipients + ) + end + defp search_ids(query, limit, offset, author) do base_sql = "SELECT id FROM #{table()} WHERE content &&& $1" params = [query] @@ -186,7 +207,7 @@ defmodule Pleroma.Search.ParadeDB do sql = base_sql <> - " ORDER BY published_at DESC LIMIT $#{limit_arg} OFFSET $#{offset_arg}" + " ORDER BY published_at DESC, id DESC LIMIT $#{limit_arg} OFFSET $#{offset_arg}" params = params ++ [limit, offset] @@ -223,11 +244,23 @@ defmodule Pleroma.Search.ParadeDB do defp dump_activity_id(<<_::binary-size(16)>> = uuid), do: uuid defp dump_activity_id(id) when is_binary(id) do - case FlakeId.Ecto.CompatType.dump(id) do - {:ok, <<_::binary-size(16)>> = uuid} -> uuid - _ -> nil + case Ecto.UUID.dump(id) do + {:ok, <<_::binary-size(16)>> = uuid} -> + uuid + + :error -> + dump_flake_activity_id(id) end end defp dump_activity_id(_), do: nil + + defp dump_flake_activity_id(id) do + case FlakeId.Ecto.CompatType.dump(id) do + {:ok, <<_::binary-size(16)>> = uuid} -> uuid + _ -> nil + end + rescue + _ -> nil + end end diff --git a/test/pleroma/search/paradedb_integration_test.exs b/test/pleroma/search/paradedb_integration_test.exs index 1da131c6b..42d12c4d6 100644 --- a/test/pleroma/search/paradedb_integration_test.exs +++ b/test/pleroma/search/paradedb_integration_test.exs @@ -14,7 +14,7 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do alias Pleroma.Search.ParadeDB alias Pleroma.Web.CommonAPI - @image "paradedb/paradedb:latest" + @image System.get_env("PARADEDB_TEST_IMAGE", "paradedb/paradedb:latest") @db_user "postgres" @db_password "postgres" @db_name "paradedb" @@ -46,7 +46,7 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do "-e", "POSTGRES_DB=#{@db_name}", "-p", - "5432", + "127.0.0.1::5432", "-d", @image ]) @@ -75,9 +75,18 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do {:ok, _pid} = start_supervised({Pleroma.Search.ParadeDB.Repo, pool_size: 1}) + previous_table = Pleroma.Config.get([Pleroma.Search.ParadeDB, :table], nil) table = "pleroma_search_documents_it_#{System.unique_integer([:positive])}" Pleroma.Config.put([Pleroma.Search.ParadeDB, :table], table) + on_exit(fn -> + if is_nil(previous_table) do + Pleroma.Config.delete([Pleroma.Search.ParadeDB, :table]) + else + Pleroma.Config.put([Pleroma.Search.ParadeDB, :table], previous_table) + end + end) + {:ok, %{ container: container, diff --git a/test/pleroma/search/paradedb_test.exs b/test/pleroma/search/paradedb_test.exs index 3338c78ef..c9bb216df 100644 --- a/test/pleroma/search/paradedb_test.exs +++ b/test/pleroma/search/paradedb_test.exs @@ -172,5 +172,68 @@ defmodule Pleroma.Search.ParadeDBTest do assert [%{id: ^activity2_id}, %{id: ^activity1_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0) end + + test "search excludes non-public activities from backend hits" do + user = insert(:user) + + {:ok, public_activity} = + CommonAPI.post(user, %{status: "public swamp", visibility: "public"}) + + {:ok, private_activity} = + CommonAPI.post(user, %{status: "private swamp", visibility: "private"}) + + public_activity_id = public_activity.id + + {:ok, dumped_public_id} = FlakeId.Ecto.CompatType.dump(public_activity.id) + {:ok, dumped_private_id} = FlakeId.Ecto.CompatType.dump(private_activity.id) + + ClientMock + |> expect(:query, fn sql, params -> + assert sql =~ "SELECT id FROM pleroma_search_documents" + assert ["swamp", _limit, _offset] = params + + {:ok, %{rows: [[dumped_private_id], [dumped_public_id]]}} + end) + + Config + |> expect(:get, 2, fn + [Pleroma.Search.ParadeDB, :client_impl], nil -> + ClientMock + + [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> + "pleroma_search_documents" + end) + + assert [%{id: ^public_activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0) + end + + test "search supports backend ids returned as uuid strings" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "uuid swamp", visibility: "public"}) + + activity_id = activity.id + {:ok, dumped_id} = FlakeId.Ecto.CompatType.dump(activity_id) + {:ok, uuid_string} = Ecto.UUID.load(dumped_id) + + ClientMock + |> expect(:query, fn sql, params -> + assert sql =~ "SELECT id FROM pleroma_search_documents" + assert ["swamp", _limit, _offset] = params + + {:ok, %{rows: [[uuid_string]]}} + end) + + Config + |> expect(:get, 2, fn + [Pleroma.Search.ParadeDB, :client_impl], nil -> + ClientMock + + [Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" -> + "pleroma_search_documents" + end) + + assert [%{id: ^activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0) + end end end