Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
10 KB
Referenced Files
None
Subscribers
None
diff --git a/changelog.d/c2s-hardening.security b/changelog.d/c2s-hardening.security
index 9bb46d986..e85bc09cd 100644
--- a/changelog.d/c2s-hardening.security
+++ b/changelog.d/c2s-hardening.security
@@ -1,3 +1,4 @@
Reject EmojiReact activities whose name is not a single emoji shortcode
Enforce poll voting rules (visible, open poll, not the author, one vote per option) for all votes on local polls and all local votes
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
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index 49a1b9238..3c9cf38e3 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -15,8 +15,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Object.Containment
+ alias Pleroma.Object.Updater
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator
@@ -40,6 +42,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator
+ alias Pleroma.Web.ActivityPub.Utils
@impl true
def validate(object, meta)
@@ -214,14 +217,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
meta
)
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
- with {_, false} <- {:local, Access.get(meta, :local, false)},
- {_, {:ok, object_data, _}} <-
+ object = maybe_rebuild_local_update_object(object, meta)
+ update_activity = Map.put(update_activity, "object", object)
+
+ with {_, {:ok, object_data, _}} <-
{:object_validation,
object
|> CommonFixes.maybe_set_attributed_to_from_activity(update_activity)
|> CommonFixes.maybe_add_language_from_activity(update_activity)
|> validate(meta)},
- meta = Keyword.put(meta, :object_data, object_data),
+ meta = maybe_put_update_object_data(meta, object_data),
{:ok, update_activity} <-
update_activity
|> UpdateValidator.cast_and_validate(meta)
@@ -229,20 +234,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
update_activity = stringify_keys(update_activity)
{:ok, update_activity, meta}
else
- {:local, _} ->
- with {:ok, object} <-
- update_activity
- |> UpdateValidator.cast_and_validate(meta)
- |> Ecto.Changeset.apply_action(:insert) do
- object = stringify_keys(object)
- {:ok, object, meta}
- end
-
- {:object_validation, e} ->
- e
-
- {:error, %Ecto.Changeset{} = e} ->
- {:error, e}
+ {:object_validation, e} -> e
+ {:error, %Ecto.Changeset{} = e} -> {:error, e}
end
end
@@ -310,6 +303,34 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
defp validate_new_object(_object, _meta), 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
+ with true <- Keyword.get(meta, :local, false),
+ %Object{data: orig_data} <- Object.get_by_ap_id(id) do
+ object =
+ object
+ |> Map.delete("updated")
+ |> Maps.put_if_present("updated", orig_data["updated"])
+
+ Updater.make_update_object_data(orig_data, object, Utils.make_date())
+ else
+ _ -> object
+ end
+ end
+
+ defp maybe_rebuild_local_update_object(object, _meta), do: object
+
+ # Local Updates keep the embedded object, so MRF filters what gets stored
+ # and federated. Remote Updates store the validated object.
+ defp maybe_put_update_object_data(meta, object_data) do
+ if Keyword.get(meta, :local, false) do
+ meta
+ else
+ Keyword.put(meta, :object_data, object_data)
+ end
+ end
+
defp validate_updated_actor(%{"object" => %{"type" => type} = actor}, meta)
when type in actor_types() do
with {_, false} <- {:local, Access.get(meta, :local, false)},
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
index a4f1e19c0..44dc33c60 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -255,7 +255,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
defp get_language_from_content_map(_), do: nil
- defp get_language_from_content(%{"content" => content} = object) do
+ defp get_language_from_content(%{"content" => content} = object) when is_binary(content) do
LanguageDetector.detect("#{object["summary"] || ""} #{content}")
end
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 c62160738..e84942303 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
@@ -14,6 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerC2STest do
import Pleroma.Factory
+ require Pleroma.Constants
+
+ @public Pleroma.Constants.as_public()
+
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
@@ -185,4 +189,70 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerC2STest do
assert featured(alice) == []
end
end
+
+ describe "Update of own objects" do
+ setup do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{status: "hello"})
+ %{user: user, object_id: activity.data["object"]}
+ end
+
+ defp update(object),
+ do: %{"type" => "Update", "to" => [@public], "cc" => [], "object" => object}
+
+ test "rejects an invalid object and keeps the post", %{conn: conn} = ctx do
+ for bad <- [%{"content" => %{"not" => "a string"}}, %{"attachment" => "garbage"}] do
+ object = Map.merge(%{"id" => ctx.object_id, "type" => "Note", "content" => "x"}, bad)
+ post_outbox(conn, ctx.user, update(object), 400)
+ end
+
+ assert Object.get_by_ap_id(ctx.object_id).data["content"] == "hello"
+ end
+
+ test "builds the edit history and the edit time on the server", %{conn: conn} = ctx do
+ forged = %{
+ "type" => "OrderedCollection",
+ "totalItems" => 1,
+ "orderedItems" => [%{"type" => "Note", "content" => "forged", "actor" => "x"}]
+ }
+
+ object = %{
+ "id" => ctx.object_id,
+ "type" => "Note",
+ "content" => "now",
+ "updated" => "2999-01-01T00:00:00Z",
+ "formerRepresentations" => forged
+ }
+
+ post_outbox(conn, ctx.user, update(object), 201)
+
+ data = Object.get_by_ap_id(ctx.object_id).data
+ assert data["content"] == "now"
+ assert [%{"content" => "hello"}] = data["formerRepresentations"]["orderedItems"]
+ refute data["updated"] =~ "2999"
+ end
+
+ test "keeps the vote counts of a local poll", %{conn: conn, user: user} do
+ {:ok, activity} =
+ CommonAPI.post(user, %{status: "poll", poll: %{options: ["a", "b"], expires_in: 600}})
+
+ %{data: data} = Object.normalize(activity, fetch: false)
+ forged = Enum.map(data["oneOf"], &put_in(&1, ["replies", "totalItems"], 1000))
+
+ object = %{
+ "id" => data["id"],
+ "type" => "Question",
+ "content" => "poll edited",
+ "oneOf" => forged,
+ "votersCount" => 2000
+ }
+
+ post_outbox(conn, user, update(object), 201)
+
+ stored = Object.get_by_ap_id(data["id"]).data
+ assert stored["content"] == "poll edited"
+ assert stored["votersCount"] == data["votersCount"]
+ assert Enum.all?(stored["oneOf"], &(&1["replies"]["totalItems"] == 0))
+ end
+ end
end
diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs
index 9dec315b3..50dbab587 100644
--- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs
@@ -155,6 +155,40 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
assert is_nil(meta[:object_data])
end
+ test "a local Update gets its history and poll results from the stored object" do
+ user = insert(:user)
+ note = insert(:note, user: user)
+
+ forged_history = %{
+ "type" => "OrderedCollection",
+ "totalItems" => 1,
+ "orderedItems" => [%{"type" => "Note", "content" => "forged"}]
+ }
+
+ updated_note =
+ note.data
+ |> Map.put("content", "edited content")
+ |> Map.put("formerRepresentations", forged_history)
+ |> Map.put("updated", "2999-01-01T00:00:00Z")
+
+ {:ok, update, _} = Builder.update(user, updated_note)
+
+ assert {:ok, %{"object" => object}, _} = ObjectValidator.validate(update, local: true)
+ assert object["content"] == "edited content"
+ assert [%{"content" => content}] = object["formerRepresentations"]["orderedItems"]
+ assert content == note.data["content"]
+ refute object["updated"] =~ "2999"
+ end
+
+ test "a local Update with an invalid object is rejected" do
+ user = insert(:user)
+ note = insert(:note, user: user)
+
+ {:ok, update, _} = Builder.update(user, Map.put(note.data, "attachment", "garbage"))
+
+ assert {:error, _} = ObjectValidator.validate(update, local: true)
+ end
+
test "returns object_data in meta for a remote Update" do
user = insert(:user)
note = insert(:note, user: user)

File Metadata

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

Event Timeline