Merge branch 'security/spoofing-hardening' into 'develop'
security: spoofing hardening Closes #380, #381, and #382 See merge request pleroma/pleroma!461
This commit is contained in:
commit
d73c7cc0ca
7 changed files with 195 additions and 17 deletions
|
@ -628,9 +628,7 @@ def user_data_from_user_object(data) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_and_prepare_user_from_ap_id(ap_id) do
|
def fetch_and_prepare_user_from_ap_id(ap_id) do
|
||||||
with {:ok, %{status_code: 200, body: body}} <-
|
with {:ok, data} <- fetch_and_contain_remote_object_from_id(ap_id) do
|
||||||
@httpoison.get(ap_id, [Accept: "application/activity+json"], follow_redirect: true),
|
|
||||||
{:ok, data} <- Jason.decode(body) do
|
|
||||||
user_data_from_user_object(data)
|
user_data_from_user_object(data)
|
||||||
else
|
else
|
||||||
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||||
|
@ -732,16 +730,7 @@ def fetch_object_from_id(id) do
|
||||||
else
|
else
|
||||||
Logger.info("Fetching #{id} via AP")
|
Logger.info("Fetching #{id} via AP")
|
||||||
|
|
||||||
with true <- String.starts_with?(id, "http"),
|
with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
|
||||||
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
|
|
||||||
@httpoison.get(
|
|
||||||
id,
|
|
||||||
[Accept: "application/activity+json"],
|
|
||||||
follow_redirect: true,
|
|
||||||
timeout: 10000,
|
|
||||||
recv_timeout: 20000
|
|
||||||
),
|
|
||||||
{:ok, data} <- Jason.decode(body),
|
|
||||||
nil <- Object.normalize(data),
|
nil <- Object.normalize(data),
|
||||||
params <- %{
|
params <- %{
|
||||||
"type" => "Create",
|
"type" => "Create",
|
||||||
|
@ -771,6 +760,27 @@ def fetch_object_from_id(id) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch_and_contain_remote_object_from_id(id) do
|
||||||
|
Logger.info("Fetching #{id} via AP")
|
||||||
|
|
||||||
|
with true <- String.starts_with?(id, "http"),
|
||||||
|
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
|
||||||
|
@httpoison.get(
|
||||||
|
id,
|
||||||
|
[Accept: "application/activity+json"],
|
||||||
|
follow_redirect: true,
|
||||||
|
timeout: 10000,
|
||||||
|
recv_timeout: 20000
|
||||||
|
),
|
||||||
|
{:ok, data} <- Jason.decode(body),
|
||||||
|
:ok <- Transmogrifier.contain_origin_from_id(id, data) do
|
||||||
|
{:ok, data}
|
||||||
|
else
|
||||||
|
e ->
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def is_public?(activity) do
|
def is_public?(activity) do
|
||||||
"https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
|
"https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
|
||||||
(activity.data["cc"] || []))
|
(activity.data["cc"] || []))
|
||||||
|
|
|
@ -50,6 +50,19 @@ def contain_origin(id, %{"actor" => actor} = params) do
|
||||||
end
|
end
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
|
@ -454,15 +467,20 @@ def handle_incoming(
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Make secure.
|
# TODO: We presently assume that any actor on the same origin domain as the object being
|
||||||
|
# deleted has the rights to delete that object. A better way to validate whether or not
|
||||||
|
# the object should be deleted is to refetch the object URI, which should return either
|
||||||
|
# an error or a tombstone. This would allow us to verify that a deletion actually took
|
||||||
|
# place.
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data
|
%{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
|
||||||
) 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 <- 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, activity} <- ActivityPub.delete(object, false) do
|
{:ok, activity} <- ActivityPub.delete(object, false) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
|
|
|
@ -101,17 +101,23 @@ def handle(:incoming_ap_doc, params) do
|
||||||
|
|
||||||
params = Utils.normalize_params(params)
|
params = Utils.normalize_params(params)
|
||||||
|
|
||||||
|
# NOTE: we use the actor ID to do the containment, this is fine because an
|
||||||
|
# actor shouldn't be acting on objects outside their own AP server.
|
||||||
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
|
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
|
||||||
nil <- Activity.normalize(params["id"]),
|
nil <- Activity.normalize(params["id"]),
|
||||||
{:ok, _activity} <- Transmogrifier.handle_incoming(params) do
|
:ok <- Transmogrifier.contain_origin_from_id(params["actor"], params),
|
||||||
|
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
|
||||||
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
%Activity{} ->
|
%Activity{} ->
|
||||||
Logger.info("Already had #{params["id"]}")
|
Logger.info("Already had #{params["id"]}")
|
||||||
|
:error
|
||||||
|
|
||||||
_e ->
|
_e ->
|
||||||
# Just drop those for now
|
# Just drop those for now
|
||||||
Logger.info("Unhandled activity")
|
Logger.info("Unhandled activity")
|
||||||
Logger.info(Poison.encode!(params, pretty: 2))
|
Logger.info(Poison.encode!(params, pretty: 2))
|
||||||
|
:error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
13
test/fixtures/httpoison_mock/https__info.pleroma.site_activity4.json
vendored
Normal file
13
test/fixtures/httpoison_mock/https__info.pleroma.site_activity4.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"attributedTo": "http://mastodon.example.org/users/admin",
|
||||||
|
"attachment": [],
|
||||||
|
"content": "<p>this post was not actually written by Haelwenn</p>",
|
||||||
|
"id": "http://mastodon.example.org/users/admin/activities/1234",
|
||||||
|
"published": "2018-09-01T22:15:00Z",
|
||||||
|
"tag": [],
|
||||||
|
"to": [
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
],
|
||||||
|
"type": "Note"
|
||||||
|
}
|
|
@ -56,6 +56,14 @@ def get("https://info.pleroma.site/activity3.json", _, _) do
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get("https://info.pleroma.site/activity4.json", _, _) do
|
||||||
|
{:ok,
|
||||||
|
%Response{
|
||||||
|
status_code: 200,
|
||||||
|
body: File.read!("test/fixtures/httpoison_mock/https__info.pleroma.site_activity4.json")
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
def get("https://info.pleroma.site/actor.json", _, _) do
|
def get("https://info.pleroma.site/actor.json", _, _) do
|
||||||
{:ok,
|
{:ok,
|
||||||
%Response{
|
%Response{
|
||||||
|
@ -756,6 +764,14 @@ def get("https://niu.moe/users/rye", [Accept: "application/activity+json"], _) d
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get("https://n1u.moe/users/rye", [Accept: "application/activity+json"], _) do
|
||||||
|
{:ok,
|
||||||
|
%Response{
|
||||||
|
status_code: 200,
|
||||||
|
body: File.read!("test/fixtures/httpoison_mock/rye.json")
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
"https://mst3k.interlinked.me/users/luciferMysticus",
|
"https://mst3k.interlinked.me/users/luciferMysticus",
|
||||||
[Accept: "application/activity+json"],
|
[Accept: "application/activity+json"],
|
||||||
|
|
|
@ -361,6 +361,26 @@ test "it works for incoming deletes" do
|
||||||
refute Repo.get(Activity, activity.id)
|
refute Repo.get(Activity, activity.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it fails for incoming deletes with spoofed origin" do
|
||||||
|
activity = insert(:note_activity)
|
||||||
|
|
||||||
|
data =
|
||||||
|
File.read!("test/fixtures/mastodon-delete.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|
||||||
|
object =
|
||||||
|
data["object"]
|
||||||
|
|> Map.put("id", activity.data["object"]["id"])
|
||||||
|
|
||||||
|
data =
|
||||||
|
data
|
||||||
|
|> Map.put("object", object)
|
||||||
|
|
||||||
|
:error = Transmogrifier.handle_incoming(data)
|
||||||
|
|
||||||
|
assert Repo.get(Activity, activity.id)
|
||||||
|
end
|
||||||
|
|
||||||
test "it works for incoming unannounces with an existing notice" do
|
test "it works for incoming unannounces with an existing notice" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
|
||||||
|
@ -918,4 +938,61 @@ 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
|
||||||
|
|
|
@ -61,4 +61,42 @@ test "with relays deactivated, it does not publish to the relay", %{
|
||||||
Pleroma.Config.put([:instance, :allow_relay], true)
|
Pleroma.Config.put([:instance, :allow_relay], true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "Receive an activity" do
|
||||||
|
test "successfully processes incoming AP docs with correct origin" do
|
||||||
|
params = %{
|
||||||
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
|
"actor" => "http://mastodon.example.org/users/admin",
|
||||||
|
"type" => "Create",
|
||||||
|
"id" => "http://mastodon.example.org/users/admin/activities/1",
|
||||||
|
"object" => %{
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "hi world!",
|
||||||
|
"id" => "http://mastodon.example.org/users/admin/objects/1",
|
||||||
|
"attributedTo" => "http://mastodon.example.org/users/admin"
|
||||||
|
},
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, _activity} = Federator.handle(:incoming_ap_doc, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects incoming AP docs with incorrect origin" do
|
||||||
|
params = %{
|
||||||
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
|
"actor" => "https://niu.moe/users/rye",
|
||||||
|
"type" => "Create",
|
||||||
|
"id" => "http://mastodon.example.org/users/admin/activities/1",
|
||||||
|
"object" => %{
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "hi world!",
|
||||||
|
"id" => "http://mastodon.example.org/users/admin/objects/1",
|
||||||
|
"attributedTo" => "http://mastodon.example.org/users/admin"
|
||||||
|
},
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
|
||||||
|
}
|
||||||
|
|
||||||
|
:error = Federator.handle(:incoming_ap_doc, params)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue