commit: 2fcb90f3697d3c15e1aebab89b7eaaa69a315c0b
parent b89070a6ad2704f4bc061c22e099f662655c3e6f
Author: Lain Soykaf <lain@lain.com>
Date: Thu, 27 Feb 2025 17:06:15 +0400
Emoji, Pack, Backup, Frontend: Use SafeZip
Diffstat:
4 files changed, 71 insertions(+), 83 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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}