commit: 61e4be396f741cf45a2861e026b1e5fb3d185d99
parent 62856ab18f8992fb73cb27db40bbea9f29b5d1b6
Author: feld <feld@feld.me>
Date: Sun, 1 Sep 2024 18:08:07 +0000
Merge branch 'drop-unknown-deletes' into 'develop'
Drop unwanted activities from unknown actors
See merge request pleroma/pleroma!4236
Diffstat:
6 files changed, 175 insertions(+), 3 deletions(-)
diff --git a/changelog.d/drop-unwanted.change b/changelog.d/drop-unwanted.change
@@ -0,0 +1 @@
+Restrict incoming activities from unknown actors to a subset that does not imply a previous relationship and early rejection of unrecognized activity types.
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
@@ -85,6 +85,36 @@ defmodule Pleroma.Constants do
]
)
+ const(activity_types,
+ do: [
+ "Create",
+ "Update",
+ "Delete",
+ "Follow",
+ "Accept",
+ "Reject",
+ "Add",
+ "Remove",
+ "Like",
+ "Announce",
+ "Undo",
+ "Flag",
+ "EmojiReact"
+ ]
+ )
+
+ const(allowed_activity_types_from_strangers,
+ do: [
+ "Block",
+ "Create",
+ "Flag",
+ "Follow",
+ "Like",
+ "EmojiReact",
+ "Announce"
+ ]
+ )
+
# basic regex, just there to weed out potential mistakes
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
const(mime_regex,
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -311,7 +311,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
post_inbox_relayed_create(conn, params)
else
conn
- |> put_status(:bad_request)
+ |> put_status(403)
|> json("Not federating")
end
end
diff --git a/lib/pleroma/web/plugs/inbox_guard_plug.ex b/lib/pleroma/web/plugs/inbox_guard_plug.ex
@@ -0,0 +1,89 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.InboxGuardPlug do
+ import Plug.Conn
+ import Pleroma.Constants, only: [activity_types: 0, allowed_activity_types_from_strangers: 0]
+
+ alias Pleroma.Config
+ alias Pleroma.User
+
+ def init(options) do
+ options
+ end
+
+ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
+ with {_, true} <- {:federating, Config.get!([:instance, :federating])} do
+ conn
+ |> filter_activity_types()
+ else
+ {:federating, false} ->
+ conn
+ |> json(403, "Not federating")
+ |> halt()
+ end
+ end
+
+ def call(conn, _opts) do
+ with {_, true} <- {:federating, Config.get!([:instance, :federating])},
+ conn = filter_activity_types(conn),
+ {:known, true} <- {:known, known_actor?(conn)} do
+ conn
+ else
+ {:federating, false} ->
+ conn
+ |> json(403, "Not federating")
+ |> halt()
+
+ {:known, false} ->
+ conn
+ |> filter_from_strangers()
+ end
+ end
+
+ # Early rejection of unrecognized types
+ defp filter_activity_types(%{body_params: %{"type" => type}} = conn) do
+ with true <- type in activity_types() do
+ conn
+ else
+ _ ->
+ conn
+ |> json(400, "Invalid activity type")
+ |> halt()
+ end
+ end
+
+ # If signature failed but we know this actor we should
+ # accept it as we may only need to refetch their public key
+ # during processing
+ defp known_actor?(%{body_params: data}) do
+ case Pleroma.Object.Containment.get_actor(data) |> User.get_cached_by_ap_id() do
+ %User{} -> true
+ _ -> false
+ end
+ end
+
+ # Only permit a subset of activity types from strangers
+ # or else it will add actors you've never interacted with
+ # to the database
+ defp filter_from_strangers(%{body_params: %{"type" => type}} = conn) do
+ with true <- type in allowed_activity_types_from_strangers() do
+ conn
+ else
+ _ ->
+ conn
+ |> json(400, "Invalid activity type for an unknown actor")
+ |> halt()
+ end
+ end
+
+ defp json(conn, status, resp) do
+ json_resp = Jason.encode!(resp)
+
+ conn
+ |> put_resp_content_type("application/json")
+ |> resp(status, json_resp)
+ |> halt()
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
@@ -217,6 +217,10 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end
+ pipeline :inbox_guard do
+ plug(Pleroma.Web.Plugs.InboxGuardPlug)
+ end
+
pipeline :static_fe do
plug(Pleroma.Web.Plugs.StaticFEPlug)
end
@@ -920,7 +924,7 @@ defmodule Pleroma.Web.Router do
end
scope "/", Pleroma.Web.ActivityPub do
- pipe_through(:activitypub)
+ pipe_through([:activitypub, :inbox_guard])
post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
end
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -657,7 +657,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
end
test "without valid signature, " <>
- "it only accepts Create activities and requires enabled federation",
+ "it accepts Create activities and requires enabled federation",
%{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
@@ -684,6 +684,54 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> json_response(400)
end
+ # When activity is delivered to the inbox and we cannot immediately verify signature
+ # we capture all the params and process it later in the Oban job.
+ # Once we begin processing it through Oban we risk fetching the actor to validate the
+ # activity which just leads to inserting a new user to process a Delete not relevant to us.
+ test "Activities of certain types from an unknown actor are discarded", %{conn: conn} do
+ example_bad_types =
+ Pleroma.Constants.activity_types() --
+ Pleroma.Constants.allowed_activity_types_from_strangers()
+
+ Enum.each(example_bad_types, fn bad_type ->
+ params =
+ %{
+ "type" => bad_type,
+ "actor" => "https://unknown.mastodon.instance/users/somebody"
+ }
+ |> Jason.encode!()
+
+ conn
+ |> assign(:valid_signature, false)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/inbox", params)
+ |> json_response(400)
+
+ assert all_enqueued() == []
+ end)
+ end
+
+ test "Unknown activity types are discarded", %{conn: conn} do
+ unknown_types = ["Poke", "Read", "Dazzle"]
+
+ Enum.each(unknown_types, fn bad_type ->
+ params =
+ %{
+ "type" => bad_type,
+ "actor" => "https://unknown.mastodon.instance/users/somebody"
+ }
+ |> Jason.encode!()
+
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/inbox", params)
+ |> json_response(400)
+
+ assert all_enqueued() == []
+ end)
+ end
+
test "accepts Add/Remove activities", %{conn: conn} do
object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"