fix(fetcher): require fetched documents to have the fetched id
The fetcher accepted any document whose id was on the same host as the fetch URL. Anyone who can place a JSON file on a host, for example through an upload, could pass it off as another object or actor from that host. The id of a fetched document must now be the URL it came from (after redirects), ignoring a trailing slash, the fragment, the host case and differences that our URL encoding removes. If it differs, as for Mastodon's /@user/123 display URLs, we refetch once from that id, which must be on the same host. The refetch must not redirect and must match exactly. Test mocks that served a document under a different URL now serve it from its own id. Based on Akkoma 8684964c5 and 940792f8b.
This commit is contained in:
parent
695dd340de
commit
20354e0aee
8 changed files with 259 additions and 15 deletions
|
|
@ -108,6 +108,34 @@ defmodule Pleroma.Object.Containment do
|
|||
|
||||
def contain_origin_from_id(_id, _data), do: :error
|
||||
|
||||
@doc """
|
||||
Checks that a fetched document's id is the URL it was fetched from
|
||||
(after redirects).
|
||||
|
||||
Fragments, a trailing slash, the case of the host and differences
|
||||
that our URL encoding removes before a request are ignored.
|
||||
Documents without an id never match.
|
||||
|
||||
Based on Akkoma's `contain_id_to_fetch/2`.
|
||||
"""
|
||||
def contain_id_to_fetch(url, %{"id" => id}) when is_binary(url) and is_binary(id) do
|
||||
if normalize_uri(url) == normalize_uri(id), do: :ok, else: :error
|
||||
end
|
||||
|
||||
def contain_id_to_fetch(_url, _data), do: :error
|
||||
|
||||
defp normalize_uri(uri) do
|
||||
uri = uri |> Pleroma.Utils.URIEncoding.encode_url() |> URI.parse()
|
||||
|
||||
%URI{
|
||||
uri
|
||||
| fragment: nil,
|
||||
host: uri.host && String.downcase(uri.host, :ascii),
|
||||
path: uri.path && String.replace_suffix(uri.path, "/", "")
|
||||
}
|
||||
|> URI.to_string()
|
||||
end
|
||||
|
||||
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
|
||||
do: contain_origin(id, object)
|
||||
|
||||
|
|
|
|||
|
|
@ -140,17 +140,39 @@ defmodule Pleroma.Object.Fetcher do
|
|||
def fetch_and_contain_remote_object_from_id(%{"id" => id}),
|
||||
do: fetch_and_contain_remote_object_from_id(id)
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
||||
def fetch_and_contain_remote_object_from_id(id) when is_binary(id),
|
||||
do: fetch_and_contain(id, false)
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(_id),
|
||||
do: {:error, "id must be a string"}
|
||||
|
||||
# The fetched document's id must be the URL it came from. Display URLs
|
||||
# (e.g. Mastodon's /@user/123) serve the object under another id, so
|
||||
# if the id differs we refetch once from that id, which has to be on the
|
||||
# same host, must not redirect, and must then match exactly.
|
||||
# Based on Akkoma 8684964c5 and 940792f8b.
|
||||
defp fetch_and_contain(id, canonical?) do
|
||||
Logger.debug("Fetching object #{id} via AP")
|
||||
|
||||
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, final_url, body} <- get_object(id),
|
||||
{_, true} <- {:redirect, not canonical? or same_url?(final_url, id)},
|
||||
{:ok, data} <- safe_json_decode(body),
|
||||
:ok <- Containment.contain_origin_from_id(id, data) do
|
||||
:ok <- Containment.contain_origin_from_id(id, data),
|
||||
{_, _, :ok} <- {:id, data, Containment.contain_id_to_fetch(final_url, data)} do
|
||||
{:ok, data}
|
||||
else
|
||||
{:id, %{"id" => canonical_id}, :error} when is_binary(canonical_id) and not canonical? ->
|
||||
fetch_and_contain(canonical_id, true)
|
||||
|
||||
{:id, _, :error} ->
|
||||
{:error, :id_mismatch}
|
||||
|
||||
{:redirect, false} ->
|
||||
{:error, :canonical_id_redirect}
|
||||
|
||||
{:scheme, _} ->
|
||||
{:error, "Unsupported URI scheme"}
|
||||
|
||||
|
|
@ -168,8 +190,7 @@ defmodule Pleroma.Object.Fetcher do
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(_id),
|
||||
do: {:error, "id must be a string"}
|
||||
defp same_url?(url, other), do: Containment.contain_id_to_fetch(url, %{"id" => other}) == :ok
|
||||
|
||||
defp check_crossdomain_redirect(final_host, _original_url) when is_nil(final_host) do
|
||||
{:cross_domain_redirect, false}
|
||||
|
|
@ -195,7 +216,7 @@ defmodule Pleroma.Object.Fetcher do
|
|||
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}
|
||||
{:ok, if(final_url in [nil, ""], do: id, else: final_url), body}
|
||||
else
|
||||
{:cross_domain_redirect, true} ->
|
||||
{:error, {:cross_domain_redirect, true}}
|
||||
|
|
@ -209,7 +230,7 @@ defmodule Pleroma.Object.Fetcher do
|
|||
case List.keyfind(headers, "content-type", 0) do
|
||||
{_, content_type} ->
|
||||
case verify_content_type(content_type) do
|
||||
{:ok, _} -> {:ok, body}
|
||||
{:ok, _} -> {:ok, id, body}
|
||||
error -> error
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue