summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/config.ex1
-rw-r--r--lib/pleroma/config/getting.ex3
-rw-r--r--lib/pleroma/date_time.ex3
-rw-r--r--lib/pleroma/date_time/impl.ex6
-rw-r--r--lib/pleroma/mogrify_behaviour.ex15
-rw-r--r--lib/pleroma/mogrify_wrapper.ex30
-rw-r--r--lib/pleroma/object/containment.ex13
-rw-r--r--lib/pleroma/object/fetcher.ex62
-rw-r--r--lib/pleroma/reverse_proxy.ex18
-rw-r--r--lib/pleroma/upload/filter/anonymize_filename.ex4
-rw-r--r--lib/pleroma/upload/filter/mogrify.ex17
-rw-r--r--lib/pleroma/user_relationship.ex10
-rw-r--r--lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex24
-rw-r--r--lib/pleroma/web/plugs/instance_static.ex22
-rw-r--r--lib/pleroma/web/plugs/uploaded_media.ex17
-rw-r--r--lib/pleroma/web/plugs/utils.ex14
16 files changed, 228 insertions, 31 deletions
diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex
index cf1453c9b..1bc371dec 100644
--- a/lib/pleroma/config.ex
+++ b/lib/pleroma/config.ex
@@ -27,6 +27,7 @@ defmodule Pleroma.Config do
Application.get_env(:pleroma, key, default)
end
+ @impl true
def get!(key) do
value = get(key, nil)
diff --git a/lib/pleroma/config/getting.ex b/lib/pleroma/config/getting.ex
index ec93fd02a..adf764f89 100644
--- a/lib/pleroma/config/getting.ex
+++ b/lib/pleroma/config/getting.ex
@@ -5,10 +5,13 @@
defmodule Pleroma.Config.Getting do
@callback get(any()) :: any()
@callback get(any(), any()) :: any()
+ @callback get!(any()) :: any()
def get(key), do: get(key, nil)
def get(key, default), do: impl().get(key, default)
+ def get!(key), do: impl().get!(key)
+
def impl do
Application.get_env(:pleroma, :config_impl, Pleroma.Config)
end
diff --git a/lib/pleroma/date_time.ex b/lib/pleroma/date_time.ex
new file mode 100644
index 000000000..d79cb848b
--- /dev/null
+++ b/lib/pleroma/date_time.ex
@@ -0,0 +1,3 @@
+defmodule Pleroma.DateTime do
+ @callback utc_now() :: NaiveDateTime.t()
+end
diff --git a/lib/pleroma/date_time/impl.ex b/lib/pleroma/date_time/impl.ex
new file mode 100644
index 000000000..102be047b
--- /dev/null
+++ b/lib/pleroma/date_time/impl.ex
@@ -0,0 +1,6 @@
+defmodule Pleroma.DateTime.Impl do
+ @behaviour Pleroma.DateTime
+
+ @impl true
+ def utc_now, do: NaiveDateTime.utc_now()
+end
diff --git a/lib/pleroma/mogrify_behaviour.ex b/lib/pleroma/mogrify_behaviour.ex
new file mode 100644
index 000000000..234cb86cf
--- /dev/null
+++ b/lib/pleroma/mogrify_behaviour.ex
@@ -0,0 +1,15 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MogrifyBehaviour do
+ @moduledoc """
+ Behaviour for Mogrify operations.
+ This module defines the interface for Mogrify operations that can be mocked in tests.
+ """
+
+ @callback open(binary()) :: map()
+ @callback custom(map(), binary()) :: map()
+ @callback custom(map(), binary(), binary()) :: map()
+ @callback save(map(), keyword()) :: map()
+end
diff --git a/lib/pleroma/mogrify_wrapper.ex b/lib/pleroma/mogrify_wrapper.ex
new file mode 100644
index 000000000..17174fd97
--- /dev/null
+++ b/lib/pleroma/mogrify_wrapper.ex
@@ -0,0 +1,30 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MogrifyWrapper do
+ @moduledoc """
+ Default implementation of MogrifyBehaviour that delegates to Mogrify.
+ """
+ @behaviour Pleroma.MogrifyBehaviour
+
+ @impl true
+ def open(file) do
+ Mogrify.open(file)
+ end
+
+ @impl true
+ def custom(image, action) do
+ Mogrify.custom(image, action)
+ end
+
+ @impl true
+ def custom(image, action, options) do
+ Mogrify.custom(image, action, options)
+ end
+
+ @impl true
+ def save(image, opts) do
+ Mogrify.save(image, opts)
+ end
+end
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index f6106cb3f..77fac12c0 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -48,6 +48,19 @@ defmodule Pleroma.Object.Containment do
defp compare_uris(_id_uri, _other_uri), do: :error
@doc """
+ Checks whether an URL to fetch from is from the local server.
+
+ We never want to fetch from ourselves; if it's not in the database
+ it can't be authentic and must be a counterfeit.
+ """
+ def contain_local_fetch(id) do
+ case compare_uris(URI.parse(id), Pleroma.Web.Endpoint.struct_url()) do
+ :ok -> :error
+ _ -> :ok
+ end
+ end
+
+ @doc """
Checks that an imported AP object's actor matches the host it came from.
"""
def contain_origin(_id, %{"actor" => nil}), do: :error
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index c85a8b09f..b54ef9ce5 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -19,6 +19,8 @@ defmodule Pleroma.Object.Fetcher do
require Logger
require Pleroma.Constants
+ @mix_env Mix.env()
+
@spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
defp reinject_object(%Object{data: %{}} = object, new_data) do
Logger.debug("Reinjecting object #{new_data["id"]}")
@@ -146,6 +148,7 @@ defmodule Pleroma.Object.Fetcher do
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
{_, true} <- {:mrf, MRF.id_filter(id)},
+ {_, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)},
{:ok, body} <- get_object(id),
{:ok, data} <- safe_json_decode(body),
:ok <- Containment.contain_origin_from_id(id, data) do
@@ -158,6 +161,9 @@ defmodule Pleroma.Object.Fetcher do
{:scheme, _} ->
{:error, "Unsupported URI scheme"}
+ {:local_fetch, _} ->
+ {:error, "Trying to fetch local resource"}
+
{:error, e} ->
{:error, e}
@@ -172,6 +178,19 @@ defmodule Pleroma.Object.Fetcher do
def fetch_and_contain_remote_object_from_id(_id),
do: {:error, "id must be a string"}
+ defp check_crossdomain_redirect(final_host, original_url)
+
+ # Handle the common case in tests where responses don't include URLs
+ if @mix_env == :test do
+ defp check_crossdomain_redirect(nil, _) do
+ {:cross_domain_redirect, false}
+ end
+ end
+
+ defp check_crossdomain_redirect(final_host, original_url) do
+ {:cross_domain_redirect, final_host != URI.parse(original_url).host}
+ end
+
defp get_object(id) do
date = Pleroma.Signature.signed_date()
@@ -181,19 +200,29 @@ defmodule Pleroma.Object.Fetcher do
|> sign_fetch(id, date)
case HTTP.get(id, headers) do
+ {:ok, %{body: body, status: code, headers: headers, url: final_url}}
+ when code in 200..299 ->
+ remote_host = if final_url, do: URI.parse(final_url).host, else: nil
+
+ with {:cross_domain_redirect, false} <- check_crossdomain_redirect(remote_host, id),
+ {_, content_type} <- List.keyfind(headers, "content-type", 0),
+ {:ok, _media_type} <- verify_content_type(content_type) do
+ {:ok, body}
+ else
+ {:cross_domain_redirect, true} ->
+ {:error, {:cross_domain_redirect, true}}
+
+ error ->
+ error
+ end
+
+ # Handle the case where URL is not in the response (older HTTP library versions)
{:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
case List.keyfind(headers, "content-type", 0) do
{_, content_type} ->
- case Plug.Conn.Utils.media_type(content_type) do
- {:ok, "application", "activity+json", _} ->
- {:ok, body}
-
- {:ok, "application", "ld+json",
- %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
- {:ok, body}
-
- _ ->
- {:error, {:content_type, content_type}}
+ case verify_content_type(content_type) do
+ {:ok, _} -> {:ok, body}
+ error -> error
end
_ ->
@@ -216,4 +245,17 @@ defmodule Pleroma.Object.Fetcher do
defp safe_json_decode(nil), do: {:ok, nil}
defp safe_json_decode(json), do: Jason.decode(json)
+
+ defp verify_content_type(content_type) do
+ case Plug.Conn.Utils.media_type(content_type) do
+ {:ok, "application", "activity+json", _} ->
+ {:ok, :activity_json}
+
+ {:ok, "application", "ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
+ {:ok, :ld_json}
+
+ _ ->
+ {:error, {:content_type, content_type}}
+ end
+ end
end
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index 8aec4ae58..3c82f9996 100644
--- a/lib/pleroma/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -17,6 +17,8 @@ defmodule Pleroma.ReverseProxy do
@failed_request_ttl :timer.seconds(60)
@methods ~w(GET HEAD)
+ @allowed_mime_types Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types], [])
+
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def max_read_duration_default, do: @max_read_duration
@@ -301,10 +303,26 @@ defmodule Pleroma.ReverseProxy do
headers
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|> build_resp_cache_headers(opts)
+ |> sanitise_content_type()
|> build_resp_content_disposition_header(opts)
|> Keyword.merge(Keyword.get(opts, :resp_headers, []))
end
+ defp sanitise_content_type(headers) do
+ original_ct = get_content_type(headers)
+
+ safe_ct =
+ Pleroma.Web.Plugs.Utils.get_safe_mime_type(
+ %{allowed_mime_types: @allowed_mime_types},
+ original_ct
+ )
+
+ [
+ {"content-type", safe_ct}
+ | Enum.filter(headers, fn {k, _v} -> k != "content-type" end)
+ ]
+ end
+
defp build_resp_cache_headers(headers, _opts) do
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
diff --git a/lib/pleroma/upload/filter/anonymize_filename.ex b/lib/pleroma/upload/filter/anonymize_filename.ex
index 234ccb6bb..c0ad70368 100644
--- a/lib/pleroma/upload/filter/anonymize_filename.ex
+++ b/lib/pleroma/upload/filter/anonymize_filename.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilename do
"""
@behaviour Pleroma.Upload.Filter
- alias Pleroma.Config
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
alias Pleroma.Upload
def filter(%Upload{name: name} = upload) do
@@ -23,7 +23,7 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilename do
@spec predefined_name(String.t()) :: String.t() | nil
defp predefined_name(extension) do
- with name when not is_nil(name) <- Config.get([__MODULE__, :text]),
+ with name when not is_nil(name) <- @config_impl.get([__MODULE__, :text]),
do: String.replace(name, "{extension}", extension)
end
diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex
index d1e166022..7c7431db6 100644
--- a/lib/pleroma/upload/filter/mogrify.ex
+++ b/lib/pleroma/upload/filter/mogrify.ex
@@ -8,9 +8,16 @@ defmodule Pleroma.Upload.Filter.Mogrify do
@type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
@type conversions :: conversion() | [conversion()]
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+ @mogrify_impl Application.compile_env(
+ :pleroma,
+ [__MODULE__, :mogrify_impl],
+ Pleroma.MogrifyWrapper
+ )
+
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
try do
- do_filter(file, Pleroma.Config.get!([__MODULE__, :args]))
+ do_filter(file, @config_impl.get!([__MODULE__, :args]))
{:ok, :filtered}
rescue
e in ErlangError ->
@@ -22,9 +29,9 @@ defmodule Pleroma.Upload.Filter.Mogrify do
def do_filter(file, filters) do
file
- |> Mogrify.open()
+ |> @mogrify_impl.open()
|> mogrify_filter(filters)
- |> Mogrify.save(in_place: true)
+ |> @mogrify_impl.save(in_place: true)
end
defp mogrify_filter(mogrify, nil), do: mogrify
@@ -38,10 +45,10 @@ defmodule Pleroma.Upload.Filter.Mogrify do
defp mogrify_filter(mogrify, []), do: mogrify
defp mogrify_filter(mogrify, {action, options}) do
- Mogrify.custom(mogrify, action, options)
+ @mogrify_impl.custom(mogrify, action, options)
end
defp mogrify_filter(mogrify, action) when is_binary(action) do
- Mogrify.custom(mogrify, action)
+ @mogrify_impl.custom(mogrify, action)
end
end
diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex
index 82fcc1cdd..5b48d321a 100644
--- a/lib/pleroma/user_relationship.ex
+++ b/lib/pleroma/user_relationship.ex
@@ -55,9 +55,13 @@ defmodule Pleroma.UserRelationship do
def user_relationship_mappings, do: Pleroma.UserRelationship.Type.__enum_map__()
+ def datetime_impl do
+ Application.get_env(:pleroma, :datetime_impl, Pleroma.DateTime.Impl)
+ end
+
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
user_relationship
- |> cast(params, [:relationship_type, :source_id, :target_id, :expires_at])
+ |> cast(params, [:relationship_type, :source_id, :target_id, :expires_at, :inserted_at])
|> validate_required([:relationship_type, :source_id, :target_id])
|> unique_constraint(:relationship_type,
name: :user_relationships_source_id_relationship_type_target_id_index
@@ -65,6 +69,7 @@ defmodule Pleroma.UserRelationship do
|> validate_not_self_relationship()
end
+ @spec exists?(any(), Pleroma.User.t(), Pleroma.User.t()) :: boolean()
def exists?(relationship_type, %User{} = source, %User{} = target) do
UserRelationship
|> where(relationship_type: ^relationship_type, source_id: ^source.id, target_id: ^target.id)
@@ -90,7 +95,8 @@ defmodule Pleroma.UserRelationship do
relationship_type: relationship_type,
source_id: source.id,
target_id: target.id,
- expires_at: expires_at
+ expires_at: expires_at,
+ inserted_at: datetime_impl().utc_now()
})
|> Repo.insert(
on_conflict: {:replace_all_except, [:id, :inserted_at]},
diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
index 6edfb124e..49d17d8b9 100644
--- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
@@ -20,6 +20,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
String.match?(shortcode, pattern)
end
+ defp reject_emoji?({shortcode, _url}, installed_emoji) do
+ valid_shortcode? = String.match?(shortcode, ~r/^[a-zA-Z0-9_-]+$/)
+
+ rejected_shortcode? =
+ [:mrf_steal_emoji, :rejected_shortcodes]
+ |> Config.get([])
+ |> Enum.any?(fn pattern -> shortcode_matches?(shortcode, pattern) end)
+
+ emoji_installed? = Enum.member?(installed_emoji, shortcode)
+
+ !valid_shortcode? or rejected_shortcode? or emoji_installed?
+ end
+
defp steal_emoji({shortcode, url}, emoji_dir_path) do
url = Pleroma.Web.MediaProxy.url(url)
@@ -78,16 +91,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
new_emojis =
foreign_emojis
- |> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end)
- |> Enum.reject(fn {shortcode, _url} -> String.contains?(shortcode, ["/", "\\"]) end)
- |> Enum.filter(fn {shortcode, _url} ->
- reject_emoji? =
- [:mrf_steal_emoji, :rejected_shortcodes]
- |> Config.get([])
- |> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end)
-
- !reject_emoji?
- end)
+ |> Enum.reject(&reject_emoji?(&1, installed_emoji))
|> Enum.map(&steal_emoji(&1, emoji_dir_path))
|> Enum.filter(& &1)
diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex
index 75bfdd65b..f82b9a098 100644
--- a/lib/pleroma/web/plugs/instance_static.ex
+++ b/lib/pleroma/web/plugs/instance_static.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.Plugs.InstanceStatic do
require Pleroma.Constants
+ import Plug.Conn, only: [put_resp_header: 3]
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration.
@@ -44,10 +45,31 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
end
defp call_static(conn, opts, from) do
+ # Prevent content-type spoofing by setting content_types: false
opts =
opts
|> Map.put(:from, from)
+ |> Map.put(:content_types, false)
+ conn = set_content_type(conn, conn.request_path)
+
+ # Call Plug.Static with our sanitized content-type
Plug.Static.call(conn, opts)
end
+
+ defp set_content_type(conn, "/emoji/" <> filepath) do
+ real_mime = MIME.from_path(filepath)
+
+ clean_mime =
+ Pleroma.Web.Plugs.Utils.get_safe_mime_type(%{allowed_mime_types: ["image"]}, real_mime)
+
+ put_resp_header(conn, "content-type", clean_mime)
+ end
+
+ defp set_content_type(conn, filepath) do
+ real_mime = MIME.from_path(filepath)
+ put_resp_header(conn, "content-type", real_mime)
+ end
end
+
+# I think this needs to be uncleaned except for emoji.
diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
index f1076da1b..abacf965b 100644
--- a/lib/pleroma/web/plugs/uploaded_media.ex
+++ b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
require Logger
alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.Plugs.Utils
@behaviour Plug
# no slashes
@@ -28,7 +29,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
@@ -69,13 +72,23 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
defp media_is_banned(_, _), do: false
+ defp set_content_type(conn, opts, filepath) do
+ real_mime = MIME.from_path(filepath)
+ clean_mime = Utils.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(:content_types, false)
- conn = Plug.Static.call(conn, static_opts)
+ conn =
+ conn
+ |> set_content_type(opts, conn.request_path)
+ |> Plug.Static.call(static_opts)
if conn.halted do
conn
diff --git a/lib/pleroma/web/plugs/utils.ex b/lib/pleroma/web/plugs/utils.ex
new file mode 100644
index 000000000..05e0fbe84
--- /dev/null
+++ b/lib/pleroma/web/plugs/utils.ex
@@ -0,0 +1,14 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.Utils do
+ @moduledoc """
+ Some helper functions shared across several plugs
+ """
+
+ def 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
+end