commit: 4604f2944e8997b79c683fab95217d551fa83585
parent ca3c2a4ffaf87139a044b8b5ba2f84ead8f97891
Author: Lain Soykaf <lain@lain.com>
Date: Sat, 1 Mar 2025 14:07:02 +0400
Merge branch 'pleroma-ensure-authorized-fetch' into security-2.9
Diffstat:
7 files changed, 144 insertions(+), 5 deletions(-)
diff --git a/changelog.d/ensure-authorized-fetch.security b/changelog.d/ensure-authorized-fetch.security
@@ -0,0 +1 @@
+Require HTTP signatures (if enabled) for routes used by both C2S and S2S AP API
+\ No newline at end of file
diff --git a/config/config.exs b/config/config.exs
@@ -359,7 +359,8 @@ config :pleroma, :activitypub,
follow_handshake_timeout: 500,
note_replies_output_limit: 5,
sign_object_fetches: true,
- authorized_fetch_mode: false
+ authorized_fetch_mode: false,
+ client_api_enabled: true
config :pleroma, :streamer,
workers: 3,
diff --git a/config/description.exs b/config/description.exs
@@ -1772,6 +1772,11 @@ config :pleroma, :config_description, [
type: :integer,
description: "Following handshake timeout",
suggestions: [500]
+ },
+ %{
+ key: :client_api_enabled,
+ type: :boolean,
+ description: "Allow client to server ActivityPub interactions"
}
]
},
diff --git a/lib/pleroma/web/plugs/ap_client_api_enabled_plug.ex b/lib/pleroma/web/plugs/ap_client_api_enabled_plug.ex
@@ -0,0 +1,34 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.APClientApiEnabledPlug do
+ import Plug.Conn
+ import Phoenix.Controller, only: [text: 2]
+
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+ @enabled_path [:activitypub, :client_api_enabled]
+
+ def init(options \\ []), do: Map.new(options)
+
+ def call(conn, %{allow_server: true}) do
+ if @config_impl.get(@enabled_path, false) do
+ conn
+ else
+ conn
+ |> assign(:user, nil)
+ |> assign(:token, nil)
+ end
+ end
+
+ def call(conn, _) do
+ if @config_impl.get(@enabled_path, false) do
+ conn
+ else
+ conn
+ |> put_status(:forbidden)
+ |> text("C2S not enabled")
+ |> halt()
+ end
+ end
+end
diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
@@ -19,8 +19,16 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
options
end
- def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
- conn
+ def call(%{assigns: %{valid_signature: true}} = conn, _opts), do: conn
+
+ # skip for C2S requests from authenticated users
+ def call(%{assigns: %{user: %Pleroma.User{}}} = conn, _opts) do
+ if get_format(conn) in ["json", "activity+json"] do
+ # ensure access token is provided for 2FA
+ Pleroma.Web.Plugs.EnsureAuthenticatedPlug.call(conn, %{})
+ else
+ conn
+ end
end
def call(conn, _opts) do
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
@@ -907,22 +907,37 @@ defmodule Pleroma.Web.Router do
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do
plug(:ap_service_actor)
+ plug(Pleroma.Web.Plugs.APClientApiEnabledPlug)
plug(:fetch_session)
plug(:authenticate)
plug(:after_auth)
end
+ # AP interactions used by both S2S and C2S
+ pipeline :activitypub_server_or_client do
+ plug(:ap_service_actor)
+ plug(:fetch_session)
+ plug(:authenticate)
+ plug(Pleroma.Web.Plugs.APClientApiEnabledPlug, allow_server: true)
+ plug(:after_auth)
+ plug(:http_signature)
+ end
+
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
- get("/users/:nickname/outbox", ActivityPubController, :outbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
+ end
+
+ scope "/", Pleroma.Web.ActivityPub do
+ pipe_through([:activitypub_server_or_client])
+
+ get("/users/:nickname/outbox", ActivityPubController, :outbox)
- # The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -1344,6 +1344,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
end
describe "GET /users/:nickname/outbox" do
+ setup do
+ Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Config)
+ :ok
+ end
+
test "it paginates correctly", %{conn: conn} do
user = insert(:user)
conn = assign(conn, :user, user)
@@ -1432,6 +1437,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert %{"orderedItems" => []} = resp
end
+ test "it does not return a local note activity when C2S API is disabled", %{conn: conn} do
+ clear_config([:activitypub, :client_api_enabled], false)
+ user = insert(:user)
+ reader = insert(:user)
+ {:ok, _note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"})
+
+ resp =
+ conn
+ |> assign(:user, reader)
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/users/#{user.nickname}/outbox?page=true")
+ |> json_response(200)
+
+ assert %{"orderedItems" => []} = resp
+ end
+
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
@@ -1483,6 +1504,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert [answer_outbox] = outbox_get["orderedItems"]
assert answer_outbox["id"] == activity.data["id"]
end
+
+ test "it works with authorized fetch forced when authenticated" do
+ clear_config([:activitypub, :authorized_fetch_mode], true)
+
+ user = insert(:user)
+ outbox_endpoint = user.ap_id <> "/outbox"
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> put_req_header("accept", "application/activity+json")
+ |> get(outbox_endpoint)
+
+ assert json_response(conn, 200)
+ end
+
+ test "it fails with authorized fetch forced when unauthenticated", %{conn: conn} do
+ clear_config([:activitypub, :authorized_fetch_mode], true)
+
+ user = insert(:user)
+ outbox_endpoint = user.ap_id <> "/outbox"
+
+ conn =
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get(outbox_endpoint)
+
+ assert response(conn, 401)
+ end
end
describe "POST /users/:nickname/outbox (C2S)" do
@@ -2153,6 +2203,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> json_response(403)
end
+
+ test "they don't work when C2S API is disabled", %{conn: conn} do
+ clear_config([:activitypub, :client_api_enabled], false)
+
+ user = insert(:user)
+
+ assert conn
+ |> assign(:user, user)
+ |> get("/api/ap/whoami")
+ |> response(403)
+
+ desc = "Description of the image"
+
+ image = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ assert conn
+ |> assign(:user, user)
+ |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
+ |> response(403)
+ end
end
test "pinned collection", %{conn: conn} do