From ad953143bb00d67eb981806981f8ef3e35c437e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 15 Sep 2024 14:59:06 +0200 Subject: Require HTTP signatures (if enabled) for routes used by both C2S and S2S AP API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/web/plugs/http_signature_plug.ex | 12 ++++++++++-- lib/pleroma/web/router.ex | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex index 67974599a..2e16212ce 100644 --- 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 index 0423ca9e2..ad8529a30 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -907,17 +907,30 @@ defmodule Pleroma.Web.Router do 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(: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) -- cgit v1.2.3 From 309d22aca2ec0557b27c8e3d8d12b088061e0142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 16 Sep 2024 13:33:56 +0200 Subject: Allow disabling C2S ActivityPub API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../web/plugs/ap_client_api_enabled_plug.ex | 34 ++++++++++++++++++++++ lib/pleroma/web/router.ex | 2 ++ 2 files changed, 36 insertions(+) create mode 100644 lib/pleroma/web/plugs/ap_client_api_enabled_plug.ex (limited to 'lib') diff --git a/lib/pleroma/web/plugs/ap_client_api_enabled_plug.ex b/lib/pleroma/web/plugs/ap_client_api_enabled_plug.ex new file mode 100644 index 000000000..6807673f9 --- /dev/null +++ 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 +# 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/router.ex b/lib/pleroma/web/router.ex index ad8529a30..d78a6aef4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -902,6 +902,7 @@ 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) @@ -912,6 +913,7 @@ defmodule Pleroma.Web.Router 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 -- cgit v1.2.3 From 0f5ac7e86dd36b80016a90fe8aca581a4275b71e Mon Sep 17 00:00:00 2001 From: Oneric Date: Wed, 30 Oct 2024 23:18:10 +0100 Subject: Add SafeZip module This will replace all the slightly different safety workarounds at different ZIP handling sites and ensure safety is actually consistently enforced everywhere while also making code cleaner and easiert to follow. --- lib/pleroma/safe_zip.ex | 216 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 lib/pleroma/safe_zip.ex (limited to 'lib') diff --git a/lib/pleroma/safe_zip.ex b/lib/pleroma/safe_zip.ex new file mode 100644 index 000000000..35fe2be19 --- /dev/null +++ b/lib/pleroma/safe_zip.ex @@ -0,0 +1,216 @@ +# Akkoma: Magically expressive social media +# Copyright © 2024 Akkoma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.SafeZip do + @moduledoc """ + Wraps the subset of Erlang's zip module we’d like to use + but enforces path-traversal safety everywhere and other checks. + + For convenience almost all functions accept both elixir strings and charlists, + but output elixir strings themselves. However, this means the input parameter type + can no longer be used to distinguish archive file paths from archive binary data in memory, + thus where needed both a _data and _file variant are provided. + """ + + @type text() :: String.t() | [char()] + + defp is_safe_path?(path) do + # Path accepts elixir’s chardata() + case Path.safe_relative(path) do + {:ok, _} -> true + _ -> false + end + end + + defp is_safe_type?(file_type) do + if file_type in [:regular, :directory] do + true + else + false + end + end + + defp maybe_add_file(_type, _path_charlist, nil), do: nil + + defp maybe_add_file(:regular, path_charlist, file_list), + do: [to_string(path_charlist) | file_list] + + defp maybe_add_file(_type, _path_charlist, file_list), do: file_list + + @spec check_safe_archive_and_maybe_list_files(binary() | [char()], [term()], boolean()) :: + {:ok, [String.t()]} | {:error, reason :: term()} + defp check_safe_archive_and_maybe_list_files(archive, opts, list) do + acc = if list, do: [], else: nil + + with {:ok, table} <- :zip.table(archive, opts) do + Enum.reduce_while(table, {:ok, acc}, fn + # ZIP comment + {:zip_comment, _}, acc -> + {:cont, acc} + + # File entry + {:zip_file, path, info, _comment, _offset, _comp_size}, {:ok, fl} -> + with {_, type} <- {:get_type, elem(info, 2)}, + {_, true} <- {:type, is_safe_type?(type)}, + {_, true} <- {:safe_path, is_safe_path?(path)} do + {:cont, {:ok, maybe_add_file(type, path, fl)}} + else + {:get_type, e} -> + {:halt, + {:error, "Couldn't determine file type of ZIP entry at #{path} (#{inspect(e)})"}} + + {:type, _} -> + {:halt, {:error, "Potentially unsafe file type in ZIP at: #{path}"}} + + {:safe_path, _} -> + {:halt, {:error, "Unsafe path in ZIP: #{path}"}} + end + + # new OTP version? + _, _acc -> + {:halt, {:error, "Unknown ZIP record type"}} + end) + end + end + + @spec check_safe_archive_and_list_files(binary() | [char()], [term()]) :: + {:ok, [String.t()]} | {:error, reason :: term()} + defp check_safe_archive_and_list_files(archive, opts \\ []) do + check_safe_archive_and_maybe_list_files(archive, opts, true) + end + + @spec check_safe_archive(binary() | [char()], [term()]) :: :ok | {:error, reason :: term()} + defp check_safe_archive(archive, opts \\ []) do + case check_safe_archive_and_maybe_list_files(archive, opts, false) do + {:ok, _} -> :ok + error -> error + end + end + + @spec check_safe_file_list([text()], text()) :: :ok | {:error, term()} + defp check_safe_file_list([], _), do: :ok + + defp check_safe_file_list([path | tail], cwd) do + with {_, true} <- {:path, is_safe_path?(path)}, + {_, {:ok, fstat}} <- {:stat, File.stat(Path.expand(path, cwd))}, + {_, true} <- {:type, is_safe_type?(fstat.type)} do + check_safe_file_list(tail, cwd) + else + {:path, _} -> + {:error, "Unsafe path escaping cwd: #{path}"} + + {:stat, e} -> + {:error, "Unable to check file type of #{path}: #{inspect(e)}"} + + {:type, _} -> + {:error, "Unsafe type at #{path}"} + end + end + + defp check_safe_file_list(_, _), do: {:error, "Malformed file_list"} + + @doc """ + Checks whether the archive data contais file entries for all paths from fset + + Note this really only accepts entries corresponding to regular _files_, + if a path is contained as for example an directory, this does not count as a match. + """ + @spec contains_all_data?(binary(), MapSet.t()) :: true | false + def contains_all_data?(archive_data, fset) do + with {:ok, table} <- :zip.table(archive_data) do + remaining = + Enum.reduce(table, fset, fn + {:zip_file, path, info, _comment, _offset, _comp_size}, fset -> + if elem(info, 2) == :regular do + MapSet.delete(fset, path) + else + fset + end + + _, _ -> + fset + end) + |> MapSet.size() + + if remaining == 0, do: true, else: false + else + _ -> false + end + end + + @doc """ + List all file entries in ZIP, or error if invalid or unsafe. + + Note this really only lists regular files, no directories, ZIP comments or other types! + """ + @spec list_dir_file(text()) :: {:ok, [String.t()]} | {:error, reason :: term()} + def list_dir_file(archive) do + path = to_charlist(archive) + check_safe_archive_and_list_files(path) + end + + defp stringify_zip({:ok, {fname, data}}), do: {:ok, {to_string(fname), data}} + defp stringify_zip({:ok, fname}), do: {:ok, to_string(fname)} + defp stringify_zip(ret), do: ret + + @spec zip(text(), text(), [text()], boolean()) :: + {:ok, file_name :: String.t()} + | {:ok, {file_name :: String.t(), file_data :: binary()}} + | {:error, reason :: term()} + def zip(name, file_list, cwd, memory \\ false) do + opts = [{:cwd, to_charlist(cwd)}] + opts = if memory, do: [:memory | opts], else: opts + + with :ok <- check_safe_file_list(file_list, cwd) do + file_list = for f <- file_list, do: to_charlist(f) + name = to_charlist(name) + stringify_zip(:zip.zip(name, file_list, opts)) + end + end + + @spec unzip_file(text(), text(), [text()] | nil) :: + {:ok, [String.t()]} + | {:error, reason :: term()} + | {:error, {name :: text(), reason :: term()}} + def unzip_file(archive, target_dir, file_list \\ nil) do + do_unzip(to_charlist(archive), to_charlist(target_dir), file_list) + end + + @spec unzip_data(binary(), text(), [text()] | nil) :: + {:ok, [String.t()]} + | {:error, reason :: term()} + | {:error, {name :: text(), reason :: term()}} + def unzip_data(archive, target_dir, file_list \\ nil) do + do_unzip(archive, to_charlist(target_dir), file_list) + end + + defp stringify_unzip({:ok, [{_fname, _data} | _] = filebinlist}), + do: {:ok, Enum.map(filebinlist, fn {fname, data} -> {to_string(fname), data} end)} + + defp stringify_unzip({:ok, [_fname | _] = filelist}), + do: {:ok, Enum.map(filelist, fn fname -> to_string(fname) end)} + + defp stringify_unzip({:error, {fname, term}}), do: {:error, {to_string(fname), term}} + defp stringify_unzip(ret), do: ret + + @spec do_unzip(binary() | [char()], text(), [text()] | nil) :: + {:ok, [String.t()]} + | {:error, reason :: term()} + | {:error, {name :: text(), reason :: term()}} + defp do_unzip(archive, target_dir, file_list) do + opts = + if file_list != nil do + [ + file_list: for(f <- file_list, do: to_charlist(f)), + cwd: target_dir + ] + else + [cwd: target_dir] + end + + with :ok <- check_safe_archive(archive) do + stringify_unzip(:zip.unzip(archive, opts)) + end + end +end -- cgit v1.2.3 From 2fcb90f3697d3c15e1aebab89b7eaaa69a315c0b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 27 Feb 2025 17:06:15 +0400 Subject: Emoji, Pack, Backup, Frontend: Use SafeZip --- lib/mix/tasks/pleroma/emoji.ex | 15 ++---- lib/pleroma/emoji/pack.ex | 101 ++++++++++++++++++++++------------------- lib/pleroma/frontend.ex | 22 ++------- lib/pleroma/user/backup.ex | 16 ++++--- 4 files changed, 71 insertions(+), 83 deletions(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 8b9c921c8..b656f161f 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -93,6 +93,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do ) files = fetch_and_decode!(files_loc) + files_to_unzip = for({_, f} <- files, do: f) IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) @@ -103,17 +104,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do pack_name ]) - files_to_unzip = - Enum.map( - files, - fn {_, f} -> to_charlist(f) end - ) - - {:ok, _} = - :zip.unzip(binary_archive, - cwd: String.to_charlist(pack_path), - file_list: files_to_unzip - ) + {:ok, _} = Pleroma.SafeZip.unzip_data(binary_archive, pack_path, files_to_unzip) IO.puts(IO.ANSI.format(["Writing pack.json for ", :bright, pack_name])) @@ -201,7 +192,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}") - {:ok, _} = :zip.unzip(binary_archive, cwd: String.to_charlist(tmp_pack_dir)) + {:ok, _} = Pleroma.SafeZip.unzip_data(binary_archive, tmp_pack_dir) emoji_map = Pleroma.Emoji.Loader.make_shortcode_to_file_map(tmp_pack_dir, exts) diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 785fdb8b2..cef12822c 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -25,11 +25,12 @@ defmodule Pleroma.Emoji.Pack do alias Pleroma.Emoji alias Pleroma.Emoji.Pack alias Pleroma.Utils + alias Pleroma.SafeZip @spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} def create(name) do with :ok <- validate_not_empty([name]), - dir <- Path.join(emoji_path(), name), + dir <- path_join_name_safe(emoji_path(), name), :ok <- File.mkdir(dir) do save_pack(%__MODULE__{pack_file: Path.join(dir, "pack.json")}) end @@ -65,43 +66,21 @@ defmodule Pleroma.Emoji.Pack do {:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values} def delete(name) do with :ok <- validate_not_empty([name]), - pack_path <- Path.join(emoji_path(), name) do + pack_path <- path_join_name_safe(emoji_path(), name) do File.rm_rf(pack_path) end end - @spec unpack_zip_emojies(list(tuple())) :: list(map()) - defp unpack_zip_emojies(zip_files) do - Enum.reduce(zip_files, [], fn - {_, path, s, _, _, _}, acc when elem(s, 2) == :regular -> - with( - filename <- Path.basename(path), - shortcode <- Path.basename(filename, Path.extname(filename)), - false <- Emoji.exist?(shortcode) - ) do - [%{path: path, filename: path, shortcode: shortcode} | acc] - else - _ -> acc - end - - _, acc -> - acc - end) - end - @spec add_file(t(), String.t(), Path.t(), Plug.Upload.t()) :: {:ok, t()} | {:error, File.posix() | atom()} def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do - with {:ok, zip_files} <- :zip.table(to_charlist(file.path)), - [_ | _] = emojies <- unpack_zip_emojies(zip_files), + with {:ok, zip_files} <- SafeZip.list_dir_file(file.path), + [_ | _] = emojies <- map_zip_emojies(zip_files), {:ok, tmp_dir} <- Utils.tmp_dir("emoji") do try do {:ok, _emoji_files} = - :zip.unzip( - to_charlist(file.path), - [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, String.to_charlist(tmp_dir)}] - ) + SafeZip.unzip_file(file.path, tmp_dir, Enum.map(emojies, & &1[:path])) {_, updated_pack} = Enum.map_reduce(emojies, pack, fn item, emoji_pack -> @@ -292,7 +271,7 @@ defmodule Pleroma.Emoji.Pack do @spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()} def load_pack(name) do name = Path.basename(name) - pack_file = Path.join([emoji_path(), name, "pack.json"]) + pack_file = path_join_name_safe(emoji_path(), name) |> Path.join("pack.json") with {:ok, _} <- File.stat(pack_file), {:ok, pack_data} <- File.read(pack_file) do @@ -416,10 +395,9 @@ defmodule Pleroma.Emoji.Pack do end defp create_archive_and_cache(pack, hash) do - files = [~c"pack.json" | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)] - - {:ok, {_, result}} = - :zip.zip(~c"#{pack.name}.zip", files, [:memory, cwd: to_charlist(pack.path)]) + pack_file_list = Enum.into(pack.files, [], fn {_, f} -> f end) + files = ["pack.json" | pack_file_list] + {:ok, {_, result}} = SafeZip.zip("#{pack.name}.zip", files, pack.path, true) ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file]) overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files)) @@ -478,7 +456,7 @@ defmodule Pleroma.Emoji.Pack do end defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do - file_path = Path.join(pack.path, filename) + file_path = path_join_safe(pack.path, filename) create_subdirs(file_path) with {:ok, _} <- File.copy(upload_path, file_path) do @@ -497,8 +475,8 @@ defmodule Pleroma.Emoji.Pack do end defp rename_file(pack, filename, new_filename) do - old_path = Path.join(pack.path, filename) - new_path = Path.join(pack.path, new_filename) + old_path = path_join_safe(pack.path, filename) + new_path = path_join_safe(pack.path, new_filename) create_subdirs(new_path) with :ok <- File.rename(old_path, new_path) do @@ -516,7 +494,7 @@ defmodule Pleroma.Emoji.Pack do defp remove_file(pack, shortcode) do with {:ok, filename} <- get_filename(pack, shortcode), - emoji <- Path.join(pack.path, filename), + emoji <- path_join_safe(pack.path, filename), :ok <- File.rm(emoji) do remove_dir_if_empty(emoji, filename) end @@ -534,7 +512,7 @@ defmodule Pleroma.Emoji.Pack do defp get_filename(pack, shortcode) do with %{^shortcode => filename} when is_binary(filename) <- pack.files, - file_path <- Path.join(pack.path, filename), + file_path <- path_join_safe(pack.path, filename), {:ok, _} <- File.stat(file_path) do {:ok, filename} else @@ -584,11 +562,10 @@ defmodule Pleroma.Emoji.Pack do defp unzip(archive, pack_info, remote_pack, local_pack) do with :ok <- File.mkdir_p!(local_pack.path) do - files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end) + files = Enum.map(remote_pack["files"], fn {_, path} -> path end) # Fallback cannot contain a pack.json file - files = if pack_info[:fallback], do: files, else: [~c"pack.json" | files] - - :zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files) + files = if pack_info[:fallback], do: files, else: ["pack.json" | files] + SafeZip.unzip_data(archive, local_pack.path, files) end end @@ -649,13 +626,43 @@ defmodule Pleroma.Emoji.Pack do end defp validate_has_all_files(pack, zip) do - with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do - # Check if all files from the pack.json are in the archive - pack.files - |> Enum.all?(fn {_, from_manifest} -> - List.keyfind(f_list, to_charlist(from_manifest), 0) + # Check if all files from the pack.json are in the archive + eset = + Enum.reduce(pack.files, MapSet.new(), fn + {_, file}, s -> MapSet.put(s, to_charlist(file)) end) - |> if(do: :ok, else: {:error, :incomplete}) + + if SafeZip.contains_all_data?(zip, eset), + do: :ok, + else: {:error, :incomplete} + end + + defp path_join_name_safe(dir, name) do + if to_string(name) != Path.basename(name) or name in ["..", ".", ""] do + raise "Invalid or malicious pack name: #{name}" + else + Path.join(dir, name) end end + + defp path_join_safe(dir, path) do + {:ok, safe_path} = Path.safe_relative(path) + Path.join(dir, safe_path) + end + + defp map_zip_emojies(zip_files) do + Enum.reduce(zip_files, [], fn path, acc -> + with( + filename <- Path.basename(path), + shortcode <- Path.basename(filename, Path.extname(filename)), + # note: this only checks the shortcode, if an emoji already exists on the same path, but + # with a different shortcode, the existing one will be degraded to an alias of the new + false <- Emoji.exist?(shortcode) + ) do + [%{path: path, filename: path, shortcode: shortcode} | acc] + else + _ -> acc + end + end) + end end diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index a4f427ae5..fe7f525ea 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -65,24 +65,12 @@ defmodule Pleroma.Frontend do end def unzip(zip, dest) do - with {:ok, unzipped} <- :zip.unzip(zip, [:memory]) do - File.rm_rf!(dest) - File.mkdir_p!(dest) - - Enum.each(unzipped, fn {filename, data} -> - path = filename - - new_file_path = Path.join(dest, path) - - path - |> Path.dirname() - |> then(&Path.join(dest, &1)) - |> File.mkdir_p!() + File.rm_rf!(dest) + File.mkdir_p!(dest) - if not File.dir?(new_file_path) do - File.write!(new_file_path, data) - end - end) + case Pleroma.SafeZip.unzip_data(zip, dest) do + {:ok, _} -> :ok + error -> error end end diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index cdff297a9..7e64ae791 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -22,6 +22,8 @@ defmodule Pleroma.User.Backup do alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Workers.BackupWorker + alias Pleroma.SafeZip + alias Pleroma.Upload @type t :: %__MODULE__{} @@ -179,12 +181,12 @@ defmodule Pleroma.User.Backup do end @files [ - ~c"actor.json", - ~c"outbox.json", - ~c"likes.json", - ~c"bookmarks.json", - ~c"followers.json", - ~c"following.json" + "actor.json", + "outbox.json", + "likes.json", + "bookmarks.json", + "followers.json", + "following.json" ] @spec run(t()) :: {:ok, t()} | {:error, :failed} @@ -200,7 +202,7 @@ defmodule Pleroma.User.Backup do {_, :ok} <- {:followers, followers(backup.tempdir, backup.user)}, {_, :ok} <- {:following, following(backup.tempdir, backup.user)}, {_, {:ok, _zip_path}} <- - {:zip, :zip.create(to_charlist(tempfile), @files, cwd: to_charlist(backup.tempdir))}, + {:zip, SafeZip.zip(tempfile, @files, backup.tempdir)}, {_, {:ok, %File.Stat{size: zip_size}}} <- {:filestat, File.stat(tempfile)}, {:ok, updated_backup} <- update_record(backup, %{file_size: zip_size}) do {:ok, updated_backup} -- cgit v1.2.3 From bf134664b437a9b45a193135d708cef8e803595b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 28 Feb 2025 12:53:15 +0400 Subject: PackTest: Add test for skipping emoji --- lib/pleroma/user/backup.ex | 1 - 1 file changed, 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 7e64ae791..4b3092fdb 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -23,7 +23,6 @@ defmodule Pleroma.User.Backup do alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Workers.BackupWorker alias Pleroma.SafeZip - alias Pleroma.Upload @type t :: %__MODULE__{} -- cgit v1.2.3 From 88ee3853022e2e6e71e20cb95e31d645f5a82bec Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 1 Mar 2025 17:13:47 +0400 Subject: Transmogrifier: Strip internal fields --- lib/pleroma/web/activity_pub/transmogrifier.ex | 187 +++++++++++++++---------- 1 file changed, 115 insertions(+), 72 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 4c9956c7a..1e6ee7dc8 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -43,6 +43,38 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> fix_content_map() |> fix_addressing() |> fix_summary() + |> fix_history(&fix_object/1) + end + + defp maybe_fix_object(%{"attributedTo" => _} = object), do: fix_object(object) + defp maybe_fix_object(object), do: object + + defp fix_history(%{"formerRepresentations" => %{"orderedItems" => list}} = obj, fix_fun) + when is_list(list) do + update_in(obj["formerRepresentations"]["orderedItems"], fn h -> Enum.map(h, fix_fun) end) + end + + defp fix_history(obj, _), do: obj + + defp fix_recursive(obj, fun) do + # unlike Erlang, Elixir does not support recursive inline functions + # which would allow us to avoid reconstructing this on every recursion + rec_fun = fn + obj when is_map(obj) -> fix_recursive(obj, fun) + # there may be simple AP IDs in history (or object field) + obj -> obj + end + + obj + |> fun.() + |> fix_history(rec_fun) + |> then(fn + %{"object" => object} = doc when is_map(object) -> + update_in(doc["object"], rec_fun) + + apdoc -> + apdoc + end) end def fix_summary(%{"summary" => nil} = object) do @@ -375,11 +407,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end) end - def handle_incoming(data, options \\ []) + def handle_incoming(data, options \\ []) do + data + |> fix_recursive(&strip_internal_fields/1) + |> handle_incoming_normalized(options) + end # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them # with nil ID. - def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do + defp handle_incoming_normalized( + %{"type" => "Flag", "object" => objects, "actor" => actor} = data, + _options + ) do with context <- data["context"] || Utils.generate_context_id(), content <- data["content"] || "", %User{} = actor <- User.get_cached_by_ap_id(actor), @@ -400,16 +439,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end # disallow objects with bogus IDs - def handle_incoming(%{"id" => nil}, _options), do: :error - def handle_incoming(%{"id" => ""}, _options), do: :error + defp handle_incoming_normalized(%{"id" => nil}, _options), do: :error + defp handle_incoming_normalized(%{"id" => ""}, _options), do: :error # length of https:// = 8, should validate better, but good enough for now. - def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8, - do: :error - - def handle_incoming( - %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data, - options - ) do + defp handle_incoming_normalized(%{"id" => id}, _options) + when is_binary(id) and byte_size(id) < 8, + do: :error + + defp handle_incoming_normalized( + %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data, + options + ) do actor = Containment.get_actor(data) data = @@ -451,25 +491,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "star" => "⭐" } - @doc "Rewrite misskey likes into EmojiReacts" - def handle_incoming( - %{ - "type" => "Like", - "_misskey_reaction" => reaction - } = data, - options - ) do + # Rewrite misskey likes into EmojiReacts + defp handle_incoming_normalized( + %{ + "type" => "Like", + "_misskey_reaction" => reaction + } = data, + options + ) do data |> Map.put("type", "EmojiReact") |> Map.put("content", @misskey_reactions[reaction] || reaction) - |> handle_incoming(options) + |> handle_incoming_normalized(options) end - def handle_incoming( - %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, - options - ) - when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do + defp handle_incoming_normalized( + %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, + options + ) + when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) object = @@ -492,8 +532,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming(%{"type" => type} = data, _options) - when type in ~w{Like EmojiReact Announce Add Remove} do + defp handle_incoming_normalized(%{"type" => type} = data, _options) + when type in ~w{Like EmojiReact Announce Add Remove} do with :ok <- ObjectValidator.fetch_actor_and_object(data), {:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do @@ -503,11 +543,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming( - %{"type" => type} = data, - _options - ) - when type in ~w{Update Block Follow Accept Reject} do + defp handle_incoming_normalized( + %{"type" => type} = data, + _options + ) + when type in ~w{Update Block Follow Accept Reject} do + fixed_obj = maybe_fix_object(data["object"]) + data = if fixed_obj != nil, do: %{data | "object" => fixed_obj}, else: data + with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do @@ -515,10 +558,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming( - %{"type" => "Delete"} = data, - _options - ) do + defp handle_incoming_normalized( + %{"type" => "Delete"} = data, + _options + ) do with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} @@ -541,15 +584,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming( - %{ - "type" => "Undo", - "object" => %{"type" => "Follow", "object" => followed}, - "actor" => follower, - "id" => id - } = _data, - _options - ) do + defp handle_incoming_normalized( + %{ + "type" => "Undo", + "object" => %{"type" => "Follow", "object" => followed}, + "actor" => follower, + "id" => id + } = _data, + _options + ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do @@ -560,46 +603,46 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming( - %{ - "type" => "Undo", - "object" => %{"type" => type} - } = data, - _options - ) - when type in ["Like", "EmojiReact", "Announce", "Block"] do + defp handle_incoming_normalized( + %{ + "type" => "Undo", + "object" => %{"type" => type} + } = data, + _options + ) + when type in ["Like", "EmojiReact", "Announce", "Block"] do with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} end end # For Undos that don't have the complete object attached, try to find it in our database. - def handle_incoming( - %{ - "type" => "Undo", - "object" => object - } = activity, - options - ) - when is_binary(object) do + defp handle_incoming_normalized( + %{ + "type" => "Undo", + "object" => object + } = activity, + options + ) + when is_binary(object) do with %Activity{data: data} <- Activity.get_by_ap_id(object) do activity |> Map.put("object", data) - |> handle_incoming(options) + |> handle_incoming_normalized(options) else _e -> :error end end - def handle_incoming( - %{ - "type" => "Move", - "actor" => origin_actor, - "object" => origin_actor, - "target" => target_actor - }, - _options - ) do + defp handle_incoming_normalized( + %{ + "type" => "Move", + "actor" => origin_actor, + "object" => origin_actor, + "target" => target_actor + }, + _options + ) do with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor), true <- origin_actor in target_user.also_known_as do @@ -609,7 +652,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming(_, _), do: :error + defp handle_incoming_normalized(_, _), do: :error @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil def get_obj_helper(id, options \\ []) do -- cgit v1.2.3 From 706bfffcda001236cd5df3012b745800d1b88756 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 1 Mar 2025 17:16:48 +0400 Subject: Linting --- lib/pleroma/emoji/pack.ex | 2 +- lib/pleroma/user/backup.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index cef12822c..c58748d3c 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -24,8 +24,8 @@ defmodule Pleroma.Emoji.Pack do alias Pleroma.Emoji alias Pleroma.Emoji.Pack - alias Pleroma.Utils alias Pleroma.SafeZip + alias Pleroma.Utils @spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} def create(name) do diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 4b3092fdb..244b08adb 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -16,13 +16,13 @@ defmodule Pleroma.User.Backup do alias Pleroma.Bookmark alias Pleroma.Config alias Pleroma.Repo + alias Pleroma.SafeZip alias Pleroma.Uploaders.Uploader alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Workers.BackupWorker - alias Pleroma.SafeZip @type t :: %__MODULE__{} -- cgit v1.2.3