forked from AkkomaGang/akkoma
object: move object containment out of transmogrifier into it's own module
This commit is contained in:
parent
1a360a4eaa
commit
e8caecb5c7
5 changed files with 144 additions and 119 deletions
64
lib/pleroma/object/containment.ex
Normal file
64
lib/pleroma/object/containment.ex
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
defmodule Pleroma.Object.Containment do
|
||||||
|
@moduledoc """
|
||||||
|
# Object Containment
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks that an imported AP object's actor matches the domain it came from.
|
||||||
|
"""
|
||||||
|
def contain_origin(id, %{"actor" => nil}), do: :error
|
||||||
|
|
||||||
|
def contain_origin(id, %{"actor" => actor} = params) do
|
||||||
|
id_uri = URI.parse(id)
|
||||||
|
actor_uri = URI.parse(get_actor(params))
|
||||||
|
|
||||||
|
if id_uri.host == actor_uri.host do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def contain_origin_from_id(id, %{"id" => nil}), do: :error
|
||||||
|
|
||||||
|
def contain_origin_from_id(id, %{"id" => other_id} = params) do
|
||||||
|
id_uri = URI.parse(id)
|
||||||
|
other_uri = URI.parse(other_id)
|
||||||
|
|
||||||
|
if id_uri.host == other_uri.host do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,6 @@
|
||||||
defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
|
alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
|
||||||
|
alias Pleroma.Object.Containment
|
||||||
alias Pleroma.Web.ActivityPub.{Transmogrifier, MRF}
|
alias Pleroma.Web.ActivityPub.{Transmogrifier, MRF}
|
||||||
alias Pleroma.Web.WebFinger
|
alias Pleroma.Web.WebFinger
|
||||||
alias Pleroma.Web.Federator
|
alias Pleroma.Web.Federator
|
||||||
|
@ -739,7 +740,7 @@ def fetch_object_from_id(id) do
|
||||||
"actor" => data["actor"] || data["attributedTo"],
|
"actor" => data["actor"] || data["attributedTo"],
|
||||||
"object" => data
|
"object" => data
|
||||||
},
|
},
|
||||||
:ok <- Transmogrifier.contain_origin(id, params),
|
:ok <- Containment.contain_origin(id, params),
|
||||||
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
|
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
|
||||||
{:ok, Object.normalize(activity.data["object"])}
|
{:ok, Object.normalize(activity.data["object"])}
|
||||||
else
|
else
|
||||||
|
@ -773,7 +774,7 @@ def fetch_and_contain_remote_object_from_id(id) do
|
||||||
recv_timeout: 20000
|
recv_timeout: 20000
|
||||||
),
|
),
|
||||||
{:ok, data} <- Jason.decode(body),
|
{:ok, data} <- Jason.decode(body),
|
||||||
:ok <- Transmogrifier.contain_origin_from_id(id, data) do
|
:ok <- Containment.contain_origin_from_id(id, data) do
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
else
|
else
|
||||||
e ->
|
e ->
|
||||||
|
|
|
@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
"""
|
"""
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Object.Containment
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
@ -13,56 +14,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Checks that an imported AP object's actor matches the domain it came from.
|
|
||||||
"""
|
|
||||||
def contain_origin(id, %{"actor" => nil}), do: :error
|
|
||||||
|
|
||||||
def contain_origin(id, %{"actor" => actor} = params) do
|
|
||||||
id_uri = URI.parse(id)
|
|
||||||
actor_uri = URI.parse(get_actor(params))
|
|
||||||
|
|
||||||
if id_uri.host == actor_uri.host do
|
|
||||||
:ok
|
|
||||||
else
|
|
||||||
:error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def contain_origin_from_id(id, %{"id" => nil}), do: :error
|
|
||||||
|
|
||||||
def contain_origin_from_id(id, %{"id" => other_id} = params) do
|
|
||||||
id_uri = URI.parse(id)
|
|
||||||
other_uri = URI.parse(other_id)
|
|
||||||
|
|
||||||
if id_uri.host == other_uri.host do
|
|
||||||
:ok
|
|
||||||
else
|
|
||||||
:error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Modifies an incoming AP object (mastodon format) to our internal format.
|
Modifies an incoming AP object (mastodon format) to our internal format.
|
||||||
"""
|
"""
|
||||||
|
@ -99,7 +50,7 @@ def fix_addressing(map) do
|
||||||
|
|
||||||
def fix_actor(%{"attributedTo" => actor} = object) do
|
def fix_actor(%{"attributedTo" => actor} = object) do
|
||||||
object
|
object
|
||||||
|> Map.put("actor", get_actor(%{"actor" => actor}))
|
|> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
|
||||||
end
|
end
|
||||||
|
|
||||||
def fix_likes(%{"likes" => likes} = object)
|
def fix_likes(%{"likes" => likes} = object)
|
||||||
|
@ -277,7 +228,7 @@ def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8),
|
||||||
# - emoji
|
# - emoji
|
||||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
|
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
|
||||||
when objtype in ["Article", "Note", "Video", "Page"] do
|
when objtype in ["Article", "Note", "Video", "Page"] do
|
||||||
actor = get_actor(data)
|
actor = Containment.get_actor(data)
|
||||||
|
|
||||||
data =
|
data =
|
||||||
Map.put(data, "actor", actor)
|
Map.put(data, "actor", actor)
|
||||||
|
@ -360,7 +311,7 @@ defp get_follow_activity(follow_object, followed) do
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
|
%{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
|
||||||
) do
|
) do
|
||||||
with actor <- get_actor(data),
|
with actor <- Containment.get_actor(data),
|
||||||
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
|
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
|
||||||
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
|
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
|
||||||
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
|
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
|
||||||
|
@ -386,7 +337,7 @@ def handle_incoming(
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
|
%{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
|
||||||
) do
|
) do
|
||||||
with actor <- get_actor(data),
|
with actor <- Containment.get_actor(data),
|
||||||
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
|
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
|
||||||
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
|
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
|
||||||
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
|
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
|
||||||
|
@ -410,7 +361,7 @@ def handle_incoming(
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data
|
%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data
|
||||||
) do
|
) do
|
||||||
with actor <- get_actor(data),
|
with actor <- Containment.get_actor(data),
|
||||||
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
||||||
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
||||||
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
|
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
|
||||||
|
@ -423,7 +374,7 @@ def handle_incoming(
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
|
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
|
||||||
) do
|
) do
|
||||||
with actor <- get_actor(data),
|
with actor <- Containment.get_actor(data),
|
||||||
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
||||||
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
||||||
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
|
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
|
||||||
|
@ -477,10 +428,10 @@ def handle_incoming(
|
||||||
) do
|
) do
|
||||||
object_id = Utils.get_ap_id(object_id)
|
object_id = Utils.get_ap_id(object_id)
|
||||||
|
|
||||||
with actor <- get_actor(data),
|
with actor <- Containment.get_actor(data),
|
||||||
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
||||||
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
||||||
:ok <- contain_origin(actor.ap_id, object.data),
|
:ok <- Containment.contain_origin(actor.ap_id, object.data),
|
||||||
{:ok, activity} <- ActivityPub.delete(object, false) do
|
{:ok, activity} <- ActivityPub.delete(object, false) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
|
@ -496,7 +447,7 @@ def handle_incoming(
|
||||||
"id" => id
|
"id" => id
|
||||||
} = data
|
} = data
|
||||||
) do
|
) do
|
||||||
with actor <- get_actor(data),
|
with actor <- Containment.get_actor(data),
|
||||||
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
||||||
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
||||||
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
|
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
|
||||||
|
@ -566,7 +517,7 @@ def handle_incoming(
|
||||||
"id" => id
|
"id" => id
|
||||||
} = data
|
} = data
|
||||||
) do
|
) do
|
||||||
with actor <- get_actor(data),
|
with actor <- Containment.get_actor(data),
|
||||||
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
||||||
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
|
||||||
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
|
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
|
||||||
|
|
66
test/object/containment_test.exs
Normal file
66
test/object/containment_test.exs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
defmodule Pleroma.Object.ContainmentTest do
|
||||||
|
use Pleroma.DataCase
|
||||||
|
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Object.Containment
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
describe "general origin containment" do
|
||||||
|
test "contain_origin_from_id() catches obvious spoofing attempts" do
|
||||||
|
data = %{
|
||||||
|
"id" => "http://example.com/~alyssa/activities/1234.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
:error =
|
||||||
|
Containment.contain_origin_from_id(
|
||||||
|
"http://example.org/~alyssa/activities/1234.json",
|
||||||
|
data
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contain_origin_from_id() allows alternate IDs within the same origin domain" do
|
||||||
|
data = %{
|
||||||
|
"id" => "http://example.com/~alyssa/activities/1234.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
:ok =
|
||||||
|
Containment.contain_origin_from_id(
|
||||||
|
"http://example.com/~alyssa/activities/1234",
|
||||||
|
data
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contain_origin_from_id() allows matching IDs" do
|
||||||
|
data = %{
|
||||||
|
"id" => "http://example.com/~alyssa/activities/1234.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
:ok =
|
||||||
|
Containment.contain_origin_from_id(
|
||||||
|
"http://example.com/~alyssa/activities/1234.json",
|
||||||
|
data
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "users cannot be collided through fake direction spoofing attempts" do
|
||||||
|
user =
|
||||||
|
insert(:user, %{
|
||||||
|
nickname: "rye@niu.moe",
|
||||||
|
local: false,
|
||||||
|
ap_id: "https://niu.moe/users/rye",
|
||||||
|
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
|
||||||
|
})
|
||||||
|
|
||||||
|
{:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "all objects with fake directions are rejected by the object fetcher" do
|
||||||
|
{:error, _} =
|
||||||
|
ActivityPub.fetch_and_contain_remote_object_from_id(
|
||||||
|
"https://info.pleroma.site/activity4.json"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -945,61 +945,4 @@ test "it rejects activities which reference objects that have an incorrect attri
|
||||||
:error = Transmogrifier.handle_incoming(data)
|
:error = Transmogrifier.handle_incoming(data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "general origin containment" do
|
|
||||||
test "contain_origin_from_id() catches obvious spoofing attempts" do
|
|
||||||
data = %{
|
|
||||||
"id" => "http://example.com/~alyssa/activities/1234.json"
|
|
||||||
}
|
|
||||||
|
|
||||||
:error =
|
|
||||||
Transmogrifier.contain_origin_from_id(
|
|
||||||
"http://example.org/~alyssa/activities/1234.json",
|
|
||||||
data
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "contain_origin_from_id() allows alternate IDs within the same origin domain" do
|
|
||||||
data = %{
|
|
||||||
"id" => "http://example.com/~alyssa/activities/1234.json"
|
|
||||||
}
|
|
||||||
|
|
||||||
:ok =
|
|
||||||
Transmogrifier.contain_origin_from_id(
|
|
||||||
"http://example.com/~alyssa/activities/1234",
|
|
||||||
data
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "contain_origin_from_id() allows matching IDs" do
|
|
||||||
data = %{
|
|
||||||
"id" => "http://example.com/~alyssa/activities/1234.json"
|
|
||||||
}
|
|
||||||
|
|
||||||
:ok =
|
|
||||||
Transmogrifier.contain_origin_from_id(
|
|
||||||
"http://example.com/~alyssa/activities/1234.json",
|
|
||||||
data
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "users cannot be collided through fake direction spoofing attempts" do
|
|
||||||
user =
|
|
||||||
insert(:user, %{
|
|
||||||
nickname: "rye@niu.moe",
|
|
||||||
local: false,
|
|
||||||
ap_id: "https://niu.moe/users/rye",
|
|
||||||
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
|
|
||||||
})
|
|
||||||
|
|
||||||
{:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "all objects with fake directions are rejected by the object fetcher" do
|
|
||||||
{:error, _} =
|
|
||||||
ActivityPub.fetch_and_contain_remote_object_from_id(
|
|
||||||
"https://info.pleroma.site/activity4.json"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue