From f7c9793542711bfd7bc8bdf2d8be736a4eea9d15 Mon Sep 17 00:00:00 2001 From: Oneric Date: Mon, 4 Mar 2024 17:50:21 +0100 Subject: [PATCH] Sanitise Content-Type of uploads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lack thereof enables spoofing ActivityPub objects. A malicious user could upload fake activities as attachments and (if having access to remote search) trick local and remote fedi instances into fetching and processing it as a valid object. If uploads are hosted on the same domain as the instance itself, it is possible for anyone with upload access to impersonate(!) other users of the same instance. If uploads are exclusively hosted on a different domain, even the most basic check of domain of the object id and fetch url matching should prevent impersonation. However, it may still be possible to trick servers into accepting bogus users on the upload (sub)domain and bogus notes attributed to such users. Instances which later migrated to a different domain and have a permissive redirect rule in place can still be vulnerable. If — like Akkoma — the fetching server is overly permissive with redirects, impersonation still works. This was possible because Plug.Static also uses our custom MIME type mappings used for actually authentic AP objects. Provided external storage providers don’t somehow return ActivityStream Content-Types on their own, instances using those are also safe against their users being spoofed via uploads. Akkoma instances using the OnlyMedia upload filter cannot be exploited as a vector in this way — IF the fetching server validates the Content-Type of fetched objects (Akkoma itself does this already). However, restricting uploads to only multimedia files may be a bit too heavy-handed. Instead this commit will restrict the returned Content-Type headers for user uploaded files to a safe subset, falling back to generic 'application/octet-stream' for anything else. This will also protect against non-AP payloads as e.g. used in past frontend code injection attacks. It’s a slight regression in user comfort, if say PDFs are uploaded, but this trade-off seems fairly acceptable. (Note, just excluding our own custom types would offer no protection against non-AP payloads and bear a (perhaps small) risk of a silent regression should MIME ever decide to add a canonical extension for ActivityPub objects) Now, one might expect there to be other defence mechanisms besides Content-Type preventing counterfeits from being accepted, like e.g. validation of the queried URL and AP ID matching. Inserting a self-reference into our uploads is hard, but unfortunately *oma does not verify the id in such a way and happily accepts _anything_ from the same domain (without even considering redirects). E.g. Sharkey (and possibly other *keys) seem to attempt to guard against this by immediately refetching the object from its ID, but this is easily circumvented by just uploading two payloads with the ID of one linking to the other. Unfortunately *oma is thus _both_ a vector for spoofing and vulnerable to those spoof payloads, resulting in an easy way to impersonate our users. Similar flaws exists for emoji and media proxy. Subsequent commits will fix this by rigorously sanitising content types in more areas, hardening our checks, improving the default config and discouraging insecure config options. --- CHANGELOG.md | 11 +++++++++++ config/config.exs | 3 ++- config/description.exs | 13 +++++++++++++ lib/pleroma/web/plugs/uploaded_media.ex | 21 +++++++++++++++++++-- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9033050a1..1108c60e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased + +## Added + +## Changed + +## Fixed +- Critical security issue allowing Akkoma to be used as a vector for + (depending on configuration) impersonation of other users or creation + of bogus users and posts on the upload domain + ## 2024.02 ## Added diff --git a/config/config.exs b/config/config.exs index 1c531344c..723d173ec 100644 --- a/config/config.exs +++ b/config/config.exs @@ -65,7 +65,8 @@ config :pleroma, Pleroma.Upload, link_name: false, proxy_remote: false, filename_display_max_length: 30, - base_url: nil + base_url: nil, + allowed_mime_types: ["image", "audio", "video"] config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads" diff --git a/config/description.exs b/config/description.exs index e108aaae8..3ddd874f8 100644 --- a/config/description.exs +++ b/config/description.exs @@ -105,6 +105,19 @@ config :pleroma, :config_description, [ "https://cdn-host.com" ] }, + %{ + key: :allowed_mime_types, + label: "Allowed MIME types", + type: {:list, :string}, + description: + "List of MIME (main) types uploads are allowed to identify themselves with. Other types may still be uploaded, but will identify as a generic binary to clients. WARNING: Loosening this over the defaults can lead to security issues. Removing types is safe, but only add to the list if you are sure you know what you are doing.", + suggestions: [ + "image", + "audio", + "video", + "font" + ] + }, %{ key: :proxy_remote, type: :boolean, diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex index 300c33068..c0982b4af 100644 --- a/lib/pleroma/web/plugs/uploaded_media.ex +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -28,7 +28,9 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do |> Keyword.put(:at, "/__unconfigured_media_plug") |> Plug.Static.init() - %{static_plug_opts: static_plug_opts} + allowed_mime_types = Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types]) + + %{static_plug_opts: static_plug_opts, allowed_mime_types: allowed_mime_types} end def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do @@ -68,13 +70,28 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do defp media_is_banned(_, _), do: false + defp get_safe_mime_type(%{allowed_mime_types: allowed_mime_types} = _opts, mime) do + [maintype | _] = String.split(mime, "/", parts: 2) + if maintype in allowed_mime_types, do: mime, else: "application/octet-stream" + end + + defp set_content_type(conn, opts, filepath) do + real_mime = MIME.from_path(filepath) + clean_mime = get_safe_mime_type(opts, real_mime) + put_resp_header(conn, "content-type", clean_mime) + end + defp get_media(conn, {:static_dir, directory}, opts) do static_opts = Map.get(opts, :static_plug_opts) |> Map.put(:at, [@path]) |> Map.put(:from, directory) + |> Map.put(:set_content_type, false) - conn = Plug.Static.call(conn, static_opts) + conn = + conn + |> set_content_type(opts, conn.request_path) + |> Pleroma.Web.Plugs.StaticNoCT.call(static_opts) if conn.halted do conn