Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
8 KB
Referenced Files
None
Subscribers
None
diff --git a/changelog.d/c2s-hardening.security b/changelog.d/c2s-hardening.security
index a77275e41..a71038a1a 100644
--- a/changelog.d/c2s-hardening.security
+++ b/changelog.d/c2s-hardening.security
@@ -3,3 +3,4 @@ Enforce poll voting rules (visible, open poll, not the author, one vote per opti
Only allow pinning own posts, and only public or unlisted ones for local users; the featured collection skips non-public posts
Validate the object of local Updates; the server builds the edit history and edit time and keeps poll results
Hold new local polls to the poll limits and reject preset vote counts
+Reject local replies to and quotes of posts the author cannot see or quote
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index a522231ec..6ac36c3ed 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -37,6 +37,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.FlagValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.LocalObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.MoveValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
@@ -303,17 +304,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
defp validate_new_object(object, meta) do
if Keyword.get(meta, :local, false) do
- validate_new_local_object(object)
+ LocalObjectValidator.validate(object)
else
:ok
end
end
- defp validate_new_local_object(%{"type" => "Question"} = question),
- do: QuestionValidator.validate_local_poll(question)
-
- defp validate_new_local_object(_object), do: :ok
-
# The server owns the edit history, the edit time and the poll results of
# local objects. Only the editable fields come from the new version.
defp maybe_rebuild_local_update_object(%{"id" => id} = object, meta) when is_binary(id) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/local_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/local_object_validator.ex
new file mode 100644
index 000000000..00843dbe4
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/local_object_validator.ex
@@ -0,0 +1,60 @@
+# 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.LocalObjectValidator do
+ @moduledoc """
+ Rules for new objects of local users. CommonAPI builds objects that follow
+ them; C2S clients send their own objects, so the pipeline checks them.
+ Remote objects follow the rules of their own server.
+ """
+
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
+ alias Pleroma.Web.ActivityPub.Visibility
+
+ @spec validate(map()) :: :ok | {:error, String.t()}
+ def validate(%{"actor" => actor} = object) do
+ user = User.get_cached_by_ap_id(actor)
+
+ with :ok <- validate_in_reply_to(object, user),
+ :ok <- validate_quote(object, user) do
+ validate_type(object)
+ end
+ end
+
+ defp validate_type(%{"type" => "Question"} = question),
+ do: QuestionValidator.validate_local_poll(question)
+
+ defp validate_type(_object), do: :ok
+
+ # Unknown objects can't leak anything, so only known ones are checked.
+ defp validate_in_reply_to(%{"inReplyTo" => id}, user) when is_binary(id) do
+ case Object.normalize(id, fetch: false) do
+ %Object{} = object ->
+ if Visibility.visible_for_user?(object, user),
+ do: :ok,
+ else: {:error, "Can't reply to an invisible object"}
+
+ nil ->
+ :ok
+ end
+ end
+
+ defp validate_in_reply_to(_object, _user), do: :ok
+
+ defp validate_quote(%{"quoteUrl" => id}, %User{} = user) when is_binary(id) do
+ case Object.normalize(id, fetch: false) do
+ %Object{} = object ->
+ if Visibility.visible_for_user?(object, user) and Visibility.quotable?(object, user.ap_id),
+ do: :ok,
+ else: {:error, "Can't quote this object"}
+
+ nil ->
+ :ok
+ end
+ end
+
+ defp validate_quote(_object, _user), do: :ok
+end
diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
index 903804d0d..7d4b43572 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -136,6 +136,16 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
Pleroma.Config.restrict_unauthenticated_access?(:activities, cfg_key)
end
+ @doc "Whether the actor may quote the object: public posts, or own followers-only posts."
+ @spec quotable?(Object.t(), String.t()) :: boolean()
+ def quotable?(%Object{} = object, actor_ap_id) do
+ case get_visibility(object) do
+ visibility when visibility in ~w(public unlisted local) -> true
+ "private" -> object.data["actor"] == actor_ap_id
+ _ -> false
+ end
+ end
+
def get_visibility(object) do
to = object.data["to"] || []
cc = object.data["cc"] || []
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 2427d1298..0cc8ec524 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -211,21 +211,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
end
- defp can_quote?(_draft, _object, visibility) when visibility in ~w(public unlisted local) do
- true
- end
-
- defp can_quote?(draft, object, "private") do
- draft.user.ap_id == object.data["actor"]
- end
-
- defp can_quote?(_, _, _) do
- false
- end
-
+ # Checked here too, because previews skip the pipeline.
defp quoting_visibility(%__MODULE__{quote_post: %Activity{}} = draft) do
with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false),
- true <- can_quote?(draft, object, Visibility.get_visibility(object)) do
+ true <- Visibility.quotable?(object, draft.user.ap_id) do
draft
else
_ -> add_error(draft, dgettext("errors", "Cannot quote private message"))
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_c2s_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_c2s_test.exs
index b5dc0f220..b58af24c0 100644
--- a/test/pleroma/web/activity_pub/activity_pub_controller_c2s_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_controller_c2s_test.exs
@@ -323,4 +323,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerC2STest do
end
end
end
+
+ describe "replies and quotes" do
+ setup do
+ victim = insert(:user)
+ {:ok, secret} = CommonAPI.post(victim, %{status: "secret", visibility: "private"})
+ %{victim: victim, secret_id: secret.data["object"]}
+ end
+
+ defp note(extra) do
+ object =
+ Map.merge(%{"type" => "Note", "content" => "hi", "to" => [@public], "cc" => []}, extra)
+
+ %{"type" => "Create", "object" => object}
+ end
+
+ test "rejects a reply to a post the user cannot see", %{conn: conn, secret_id: id} do
+ post_outbox(conn, insert(:user), note(%{"inReplyTo" => id}), 400)
+ assert Object.get_by_ap_id(id).data["repliesCount"] in [nil, 0]
+ end
+
+ test "rejects a quote of a private post of another user", %{conn: conn} = ctx do
+ {:ok, follower, _} = Pleroma.User.follow(insert(:user), ctx.victim)
+ post_outbox(conn, follower, note(%{"quoteUrl" => ctx.secret_id}), 400)
+ assert Object.get_by_ap_id(ctx.secret_id).data["quotesCount"] in [nil, 0]
+ end
+
+ test "allows a quote of an own private post", %{conn: conn} = ctx do
+ result = post_outbox(conn, ctx.victim, note(%{"quoteUrl" => ctx.secret_id}), 201)
+ assert Object.normalize(result["object"], fetch: false).data["quoteUrl"] == ctx.secret_id
+ end
+ end
end

File Metadata

Mime Type
text/x-diff
Expires
Sun, Nov 1, 12:50 AM (15 h, 8 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1801999
Default Alt Text
(8 KB)

Event Timeline