Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85829835
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
10 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/changelog.d/c2s-hardening.security b/changelog.d/c2s-hardening.security
index a71038a1a..b7e2ef4ef 100644
--- a/changelog.d/c2s-hardening.security
+++ b/changelog.d/c2s-hardening.security
@@ -4,3 +4,4 @@ Only allow pinning own posts, and only public or unlisted ones for local users;
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
+Local posts and edits only accept own uploads as attachments, within the attachment and description limits, and emoji with shortcode names
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index 6ac36c3ed..a678b94d1 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -227,6 +227,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|> CommonFixes.maybe_set_attributed_to_from_activity(update_activity)
|> CommonFixes.maybe_add_language_from_activity(update_activity)
|> validate(meta)},
+ :ok <- validate_local_update(object_data, meta),
meta = maybe_put_update_object_data(meta, object_data),
{:ok, update_activity} <-
update_activity
@@ -236,7 +237,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
{:ok, update_activity, meta}
else
{:object_validation, e} -> e
- {:error, %Ecto.Changeset{} = e} -> {:error, e}
+ {:error, _} = error -> error
end
end
@@ -328,6 +329,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
defp maybe_rebuild_local_update_object(object, _meta), do: object
+ defp validate_local_update(%{"id" => id} = object_data, meta) do
+ with true <- Keyword.get(meta, :local, false),
+ %Object{} = orig_object <- Object.get_by_ap_id(id) do
+ LocalObjectValidator.validate_update(object_data, orig_object)
+ else
+ _ -> :ok
+ end
+ end
+
# 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
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
index 00843dbe4..05f011d27 100644
--- a/lib/pleroma/web/activity_pub/object_validators/local_object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/local_object_validator.ex
@@ -9,21 +9,37 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LocalObjectValidator do
Remote objects follow the rules of their own server.
"""
+ alias Pleroma.Config
+ alias Pleroma.Emoji
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
alias Pleroma.Web.ActivityPub.Visibility
+ require Pleroma.Constants
+
@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
+ :ok <- validate_quote(object, user),
+ :ok <- validate_attachments(object, user, []),
+ :ok <- validate_emoji(object) do
validate_type(object)
end
end
+ @doc "Checks the new version of an edited local object."
+ @spec validate_update(map(), Object.t()) :: :ok | {:error, String.t()}
+ def validate_update(%{"actor" => actor} = object, %Object{data: orig_data}) do
+ known_hrefs = Enum.flat_map(List.wrap(orig_data["attachment"]), &hrefs/1)
+
+ with :ok <- validate_attachments(object, User.get_cached_by_ap_id(actor), known_hrefs) do
+ validate_emoji(object)
+ end
+ end
+
defp validate_type(%{"type" => "Question"} = question),
do: QuestionValidator.validate_local_poll(question)
@@ -57,4 +73,65 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LocalObjectValidator do
end
defp validate_quote(_object, _user), do: :ok
+
+ # Attachments must be the author's uploads (through the upload endpoints),
+ # or already be attached to the edited object.
+ defp validate_attachments(object, user, known_hrefs) do
+ attachments = List.wrap(object["attachment"])
+ description_limit = Config.get([:instance, :description_limit])
+
+ cond do
+ length(attachments) > Config.get([:instance, :max_media_attachments]) ->
+ {:error, "Too many attachments"}
+
+ Enum.any?(attachments, &(String.length(&1["name"] || "") > description_limit)) ->
+ {:error, "Attachment description is too long"}
+
+ Enum.all?(attachments, &(known?(&1, known_hrefs) or own_upload?(&1, user))) ->
+ :ok
+
+ true ->
+ {:error, "Attachments must be own uploads"}
+ end
+ end
+
+ defp hrefs(%{"url" => urls}), do: for(%{"href" => href} <- List.wrap(urls), do: href)
+ defp hrefs(_attachment), do: []
+
+ defp known?(attachment, known_hrefs) do
+ case hrefs(attachment) do
+ [] -> false
+ hrefs -> Enum.all?(hrefs, &(&1 in known_hrefs))
+ end
+ end
+
+ defp own_upload?(%{"id" => id} = attachment, %User{} = user) when is_binary(id) do
+ with %Object{data: %{"type" => type}} = upload <- Object.get_cached_by_ap_id(id),
+ true <- type in Pleroma.Constants.upload_object_types(),
+ :ok <- Object.authorize_access(upload, user) do
+ known?(attachment, hrefs(upload.data))
+ else
+ _ -> false
+ end
+ end
+
+ defp own_upload?(_attachment, _user), do: false
+
+ # Remote images are fine (clients use emoji of other instances), but the
+ # names must be shortcodes. Installed emoji may have any name.
+ defp validate_emoji(object) do
+ tag_names = for %{"type" => "Emoji", "name" => name} <- List.wrap(object["tag"]), do: name
+ names = Map.keys(object["emoji"] || %{}) ++ tag_names
+
+ if Enum.all?(names, &valid_emoji_name?/1),
+ do: :ok,
+ else: {:error, "Invalid emoji name"}
+ end
+
+ defp valid_emoji_name?(name) when is_binary(name) do
+ name = String.trim(name, ":")
+ Emoji.custom?(":#{name}:") or Emoji.exist?(name)
+ end
+
+ defp valid_emoji_name?(_name), do: false
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 b58af24c0..29dd90e0a 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
@@ -232,6 +232,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerC2STest do
refute data["updated"] =~ "2999"
end
+ test "rejects attachments that are not own uploads", %{conn: conn} = ctx do
+ [url] = upload(insert(:user))["url"]
+
+ object = %{
+ "id" => ctx.object_id,
+ "type" => "Note",
+ "content" => "with a picture",
+ "attachment" => [%{"type" => "Document", "url" => [url]}]
+ }
+
+ post_outbox(conn, ctx.user, update(object), 400)
+ assert Object.get_by_ap_id(ctx.object_id).data["content"] == "hello"
+ 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}})
@@ -354,4 +368,71 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerC2STest do
assert Object.normalize(result["object"], fetch: false).data["quoteUrl"] == ctx.secret_id
end
end
+
+ describe "attachments and emoji of new posts" do
+ defp upload(user) do
+ file = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, upload} = Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id)
+ upload.data
+ end
+
+ defp stored_attachments(result),
+ do: Object.normalize(result["object"], fetch: false).data["attachment"]
+
+ test "accepts own uploads", %{conn: conn} do
+ user = insert(:user)
+ %{"url" => [%{"href" => href}]} = attachment = upload(user)
+
+ result = post_outbox(conn, user, note(%{"attachment" => [attachment]}), 201)
+ assert [%{"url" => [%{"href" => ^href}]}] = stored_attachments(result)
+ end
+
+ test "rejects other users' uploads and foreign URLs", %{conn: conn} do
+ user = insert(:user)
+ other_upload = upload(insert(:user))
+ [%{"href" => href}] = other_upload["url"]
+
+ for attachment <- [
+ other_upload,
+ %{"type" => "Document", "url" => href},
+ %{"type" => "Document", "url" => "https://evil.example/1.png"},
+ # an own upload id with someone else's file
+ Map.put(upload(user), "url", other_upload["url"])
+ ] do
+ post_outbox(conn, user, note(%{"attachment" => [attachment]}), 400)
+ end
+ end
+
+ test "rejects too many attachments and too long descriptions", %{conn: conn} do
+ clear_config([:instance, :max_media_attachments], 2)
+ clear_config([:instance, :description_limit], 20)
+ user = insert(:user)
+
+ attachments = for _ <- 1..3, do: upload(user)
+ post_outbox(conn, user, note(%{"attachment" => attachments}), 400)
+
+ long = Map.put(upload(user), "name", String.duplicate("n", 21))
+ post_outbox(conn, user, note(%{"attachment" => [long]}), 400)
+ end
+
+ test "accepts custom emoji with remote images", %{conn: conn} do
+ tag = emoji_tag(":blobcat:", "https://remote.example/blobcat.png")
+
+ result =
+ post_outbox(conn, insert(:user), note(%{"content" => ":blobcat:", "tag" => [tag]}), 201)
+
+ assert %{"blobcat" => "https://remote.example/blobcat.png"} =
+ Object.normalize(result["object"], fetch: false).data["emoji"]
+ end
+
+ test "rejects emoji with invalid names", %{conn: conn} do
+ tag = emoji_tag("<b>x</b> visit evil.example", "https://remote.example/x.png")
+ post_outbox(conn, insert(:user), note(%{"tag" => [tag]}), 400)
+ end
+ end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 1, 12:50 AM (17 h, 50 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1802000
Default Alt Text
(10 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline