2019-06-14 09:26:36 +00:00
|
|
|
|
# Pleroma: A lightweight social networking server
|
2021-01-13 06:49:20 +00:00
|
|
|
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
2019-06-14 09:26:36 +00:00
|
|
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
2018-12-01 22:29:41 +00:00
|
|
|
|
defmodule Pleroma.Object.Containment do
|
|
|
|
|
@moduledoc """
|
|
|
|
|
This module contains some useful functions for containing objects to specific
|
|
|
|
|
origins and determining those origins. They previously lived in the
|
|
|
|
|
ActivityPub `Transmogrifier` module.
|
|
|
|
|
|
|
|
|
|
Object containment is an important step in validating remote objects to prevent
|
|
|
|
|
spoofing, therefore removal of object containment functions is NOT recommended.
|
|
|
|
|
"""
|
Only allow exact id matches
This protects us from falling for obvious spoofs as from the current
upload exploit (unfortunately we can’t reasonably do anything about
spoofs with exact matches as was possible via emoji and proxy).
Such objects being invalid is supported by the spec, sepcifically
sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id
Anonymous objects are not relevant here (they can only exists within
parent objects iiuc) and neither is client-to-server or transient objects
(as those cannot be fetched in the first place).
This leaves us with the requirement for `id` to (a) exist and
(b) be a publicly dereferencable URI from the originating server.
This alone does not yet demand strict equivalence, but the spec then
further explains objects ought to be fetchable _via their ID_.
Meaning an object not retrievable via its ID, is invalid.
This reading is supported by the fact, e.g. GoToSocial (recently) and
Mastodon (for 6+ years) do already implement such strict ID checks,
additionally proving this doesn’t cause federation issues in practice.
However, apart from canonical IDs there can also be additional display
URLs. *omas first redirect those to their canonical location, but *keys
and Mastodon directly serve the AP representation without redirects.
Mastodon and GTS deal with this in two different ways,
but both constitute an effective countermeasure:
- Mastodon:
Unless it already is a known AP id, two fetches occur.
The first fetch just reads the `id` property and then refetches from
the id. The last fetch requires the returned id to exactly match the
URL the content was fetched from. (This can be optimised by skipping
the second fetch if it already matches)
https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168
https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af
- GTS:
Only does a single fetch and then checks if _either_ the id
_or_ url property (which can be an object) match the original fetch
URL. This relies on implementations always including their display URL
as "url" if differing from the id. For actors this is true for all
investigated implementations, for posts only Mastodon includes an
"url", but it is also the only one with a differing display URL.
https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222
Albeit Mastodon’s refetch offers higher compatibility with theoretical
implmentations using either multiple different display URL or not
denoting any of them as "url" at all, for now we chose to adopt a
GTS-like refetch-free approach to avoid additional implementation
concerns wrt to whether redirects should be allowed when fetching a
canonical AP id and potential for accidentally loosening some checks
(e.g. cross-domain refetches) for one of the fetches.
This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
|
|
|
|
|
|
|
|
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
|
|
|
|
|
2018-12-01 22:29:41 +00:00
|
|
|
|
def get_actor(%{"actor" => actor}) when is_binary(actor) do
|
|
|
|
|
actor
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def get_actor(%{"actor" => actor}) when is_list(actor) do
|
|
|
|
|
if is_binary(Enum.at(actor, 0)) do
|
|
|
|
|
Enum.at(actor, 0)
|
|
|
|
|
else
|
|
|
|
|
Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|
|
|
|
|
|> Map.get("id")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
|
|
|
|
|
id
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
|
|
|
|
|
get_actor(%{"actor" => actor})
|
|
|
|
|
end
|
|
|
|
|
|
2019-10-16 15:03:21 +00:00
|
|
|
|
def get_object(%{"object" => id}) when is_binary(id) do
|
|
|
|
|
id
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def get_object(%{"object" => %{"id" => id}}) when is_binary(id) do
|
|
|
|
|
id
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def get_object(_) do
|
|
|
|
|
nil
|
|
|
|
|
end
|
|
|
|
|
|
2020-02-25 14:34:56 +00:00
|
|
|
|
defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
|
|
|
|
|
defp compare_uris(_id_uri, _other_uri), do: :error
|
2019-10-18 15:34:36 +00:00
|
|
|
|
|
Only allow exact id matches
This protects us from falling for obvious spoofs as from the current
upload exploit (unfortunately we can’t reasonably do anything about
spoofs with exact matches as was possible via emoji and proxy).
Such objects being invalid is supported by the spec, sepcifically
sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id
Anonymous objects are not relevant here (they can only exists within
parent objects iiuc) and neither is client-to-server or transient objects
(as those cannot be fetched in the first place).
This leaves us with the requirement for `id` to (a) exist and
(b) be a publicly dereferencable URI from the originating server.
This alone does not yet demand strict equivalence, but the spec then
further explains objects ought to be fetchable _via their ID_.
Meaning an object not retrievable via its ID, is invalid.
This reading is supported by the fact, e.g. GoToSocial (recently) and
Mastodon (for 6+ years) do already implement such strict ID checks,
additionally proving this doesn’t cause federation issues in practice.
However, apart from canonical IDs there can also be additional display
URLs. *omas first redirect those to their canonical location, but *keys
and Mastodon directly serve the AP representation without redirects.
Mastodon and GTS deal with this in two different ways,
but both constitute an effective countermeasure:
- Mastodon:
Unless it already is a known AP id, two fetches occur.
The first fetch just reads the `id` property and then refetches from
the id. The last fetch requires the returned id to exactly match the
URL the content was fetched from. (This can be optimised by skipping
the second fetch if it already matches)
https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168
https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af
- GTS:
Only does a single fetch and then checks if _either_ the id
_or_ url property (which can be an object) match the original fetch
URL. This relies on implementations always including their display URL
as "url" if differing from the id. For actors this is true for all
investigated implementations, for posts only Mastodon includes an
"url", but it is also the only one with a differing display URL.
https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222
Albeit Mastodon’s refetch offers higher compatibility with theoretical
implmentations using either multiple different display URL or not
denoting any of them as "url" at all, for now we chose to adopt a
GTS-like refetch-free approach to avoid additional implementation
concerns wrt to whether redirects should be allowed when fetching a
canonical AP id and potential for accidentally loosening some checks
(e.g. cross-domain refetches) for one of the fetches.
This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
|
|
|
|
defp compare_uris_exact(uri, uri), do: :ok
|
|
|
|
|
|
|
|
|
|
defp compare_uris_exact(%URI{} = id, %URI{} = other),
|
|
|
|
|
do: compare_uris_exact(URI.to_string(id), URI.to_string(other))
|
|
|
|
|
|
|
|
|
|
defp compare_uris_exact(id_uri, other_uri)
|
|
|
|
|
when is_binary(id_uri) and is_binary(other_uri) do
|
|
|
|
|
norm_id = String.replace_suffix(id_uri, "/", "")
|
|
|
|
|
norm_other = String.replace_suffix(other_uri, "/", "")
|
|
|
|
|
if norm_id == norm_other, do: :ok, else: :error
|
|
|
|
|
end
|
|
|
|
|
|
2024-03-13 22:00:23 +00:00
|
|
|
|
@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
|
|
|
|
|
|
2018-12-01 22:29:41 +00:00
|
|
|
|
@doc """
|
2020-06-18 02:05:42 +00:00
|
|
|
|
Checks that an imported AP object's actor matches the host it came from.
|
2018-12-01 22:29:41 +00:00
|
|
|
|
"""
|
2019-04-17 09:22:32 +00:00
|
|
|
|
def contain_origin(_id, %{"actor" => nil}), do: :error
|
2018-12-01 22:29:41 +00:00
|
|
|
|
|
2019-04-17 09:22:32 +00:00
|
|
|
|
def contain_origin(id, %{"actor" => _actor} = params) do
|
2018-12-01 22:29:41 +00:00
|
|
|
|
id_uri = URI.parse(id)
|
|
|
|
|
actor_uri = URI.parse(get_actor(params))
|
|
|
|
|
|
2019-10-18 15:34:36 +00:00
|
|
|
|
compare_uris(actor_uri, id_uri)
|
2018-12-01 22:29:41 +00:00
|
|
|
|
end
|
|
|
|
|
|
2019-07-14 17:47:08 +00:00
|
|
|
|
def contain_origin(id, %{"attributedTo" => actor} = params),
|
|
|
|
|
do: contain_origin(id, Map.put(params, "actor", actor))
|
|
|
|
|
|
2024-03-13 21:21:19 +00:00
|
|
|
|
def contain_origin(_id, _data), do: :ok
|
2019-11-12 11:07:17 +00:00
|
|
|
|
|
Only allow exact id matches
This protects us from falling for obvious spoofs as from the current
upload exploit (unfortunately we can’t reasonably do anything about
spoofs with exact matches as was possible via emoji and proxy).
Such objects being invalid is supported by the spec, sepcifically
sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id
Anonymous objects are not relevant here (they can only exists within
parent objects iiuc) and neither is client-to-server or transient objects
(as those cannot be fetched in the first place).
This leaves us with the requirement for `id` to (a) exist and
(b) be a publicly dereferencable URI from the originating server.
This alone does not yet demand strict equivalence, but the spec then
further explains objects ought to be fetchable _via their ID_.
Meaning an object not retrievable via its ID, is invalid.
This reading is supported by the fact, e.g. GoToSocial (recently) and
Mastodon (for 6+ years) do already implement such strict ID checks,
additionally proving this doesn’t cause federation issues in practice.
However, apart from canonical IDs there can also be additional display
URLs. *omas first redirect those to their canonical location, but *keys
and Mastodon directly serve the AP representation without redirects.
Mastodon and GTS deal with this in two different ways,
but both constitute an effective countermeasure:
- Mastodon:
Unless it already is a known AP id, two fetches occur.
The first fetch just reads the `id` property and then refetches from
the id. The last fetch requires the returned id to exactly match the
URL the content was fetched from. (This can be optimised by skipping
the second fetch if it already matches)
https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168
https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af
- GTS:
Only does a single fetch and then checks if _either_ the id
_or_ url property (which can be an object) match the original fetch
URL. This relies on implementations always including their display URL
as "url" if differing from the id. For actors this is true for all
investigated implementations, for posts only Mastodon includes an
"url", but it is also the only one with a differing display URL.
https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222
Albeit Mastodon’s refetch offers higher compatibility with theoretical
implmentations using either multiple different display URL or not
denoting any of them as "url" at all, for now we chose to adopt a
GTS-like refetch-free approach to avoid additional implementation
concerns wrt to whether redirects should be allowed when fetching a
canonical AP id and potential for accidentally loosening some checks
(e.g. cross-domain refetches) for one of the fetches.
This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
|
|
|
|
@doc """
|
|
|
|
|
Check whether the fetch URL (after redirects) exactly (sans tralining slash) matches either
|
|
|
|
|
the canonical ActivityPub id or the objects url field (for display URLs from *key and Mastodon)
|
|
|
|
|
|
|
|
|
|
Since this is meant to be used for fetches, anonymous or transient objects are not accepted here.
|
|
|
|
|
"""
|
|
|
|
|
def contain_id_to_fetch(url, %{"id" => id} = data) when is_binary(id) do
|
|
|
|
|
with {:id, :error} <- {:id, compare_uris_exact(id, url)},
|
|
|
|
|
# "url" can be a "Link" object and this is checked before full normalisation
|
|
|
|
|
display_url <- Transmogrifier.fix_url(data)["url"],
|
|
|
|
|
true <- display_url != nil do
|
|
|
|
|
compare_uris_exact(display_url, url)
|
|
|
|
|
else
|
|
|
|
|
{:id, :ok} -> :ok
|
|
|
|
|
_ -> :error
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def contain_id_to_fetch(_url, _data), do: :error
|
|
|
|
|
|
2024-03-13 20:42:51 +00:00
|
|
|
|
@doc """
|
|
|
|
|
Check whether the object id is from the same host as another id
|
|
|
|
|
"""
|
2019-11-08 20:51:28 +00:00
|
|
|
|
def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(other_id) do
|
2018-12-01 22:29:41 +00:00
|
|
|
|
id_uri = URI.parse(id)
|
|
|
|
|
other_uri = URI.parse(other_id)
|
|
|
|
|
|
2019-10-18 15:34:36 +00:00
|
|
|
|
compare_uris(id_uri, other_uri)
|
2018-12-01 22:29:41 +00:00
|
|
|
|
end
|
2019-07-14 17:47:08 +00:00
|
|
|
|
|
2021-02-25 11:00:44 +00:00
|
|
|
|
# Mastodon pin activities don't have an id, so we check the object field, which will be pinned.
|
|
|
|
|
def contain_origin_from_id(id, %{"object" => object}) when is_binary(object) do
|
|
|
|
|
id_uri = URI.parse(id)
|
|
|
|
|
object_uri = URI.parse(object)
|
|
|
|
|
|
|
|
|
|
compare_uris(id_uri, object_uri)
|
|
|
|
|
end
|
|
|
|
|
|
2019-11-08 20:51:28 +00:00
|
|
|
|
def contain_origin_from_id(_id, _data), do: :error
|
|
|
|
|
|
2019-07-14 17:47:08 +00:00
|
|
|
|
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
|
|
|
|
|
do: contain_origin(id, object)
|
|
|
|
|
|
|
|
|
|
def contain_child(_), do: :ok
|
2024-03-15 21:31:45 +00:00
|
|
|
|
|
|
|
|
|
@doc "Checks whether two URIs belong to the same domain"
|
|
|
|
|
def same_origin(id1, id2) do
|
|
|
|
|
uri1 = URI.parse(id1)
|
|
|
|
|
uri2 = URI.parse(id2)
|
|
|
|
|
|
|
|
|
|
compare_uris(uri1, uri2)
|
|
|
|
|
end
|
2018-12-01 22:29:41 +00:00
|
|
|
|
end
|