akkoma/test/pleroma/object/containment_test.exs
Oneric fee57eb376 Move actor check into fetch_and_contain_remote_object_from_id
This brings it in line with its name and closes an,
in practice harmless, verification hole.

This was/is the only user of contain_origin making it
safe to change the behaviour on actor-less objects.

Until now refetched objects did not ensure the new actor matches the
domain of the object. We refetch polls occasionally to retrieve
up-to-date vote counts. A malicious AP server could have switched out
the poll after initial posting with a completely different post
attribute to an actor from another server.
While we indeed fell for this spoof before the commit,
it fortunately seems to have had no ill effect in practice,
since the asociated Create activity is not changed. When exposing the
actor via our REST API, we read this info from the activity not the
object.

This at first thought still keeps one avenue for exploit open though:
the updated actor can be from our own domain and a third server be
instructed to fetch the object from us. However this is foiled by an
id mismatch. By necessity of being fetchable and our longstanding
same-domain check, the id must still be from the attacker’s server.
Even the most barebone authenticity check is able to sus this out.
2024-03-25 14:05:05 -01:00

168 lines
5.3 KiB
Elixir

# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.ContainmentTest do
use Pleroma.DataCase
alias Pleroma.Object.Containment
alias Pleroma.User
import Pleroma.Factory
import ExUnit.CaptureLog
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "general origin containment" do
test "handles completly actorless objects gracefully" do
assert :ok ==
Containment.contain_origin("https://glaceon.social/statuses/123", %{
"deleted" => "2019-10-30T05:48:50.249606Z",
"formerType" => "Note",
"id" => "https://glaceon.social/statuses/123",
"type" => "Tombstone"
})
end
test "errors for spoofed actors" do
assert :error ==
Containment.contain_origin("https://glaceon.social/statuses/123", %{
"actor" => "https://otp.akkoma.dev/users/you",
"id" => "https://glaceon.social/statuses/123",
"type" => "Note"
})
end
test "errors for spoofed attributedTo" do
assert :error ==
Containment.contain_origin("https://glaceon.social/statuses/123", %{
"attributedTo" => "https://otp.akkoma.dev/users/you",
"id" => "https://glaceon.social/statuses/123",
"type" => "Note"
})
end
test "accepts valid actors" do
assert :ok ==
Containment.contain_origin("https://glaceon.social/statuses/123", %{
"actor" => "https://glaceon.social/users/monorail",
"attributedTo" => "https://glaceon.social/users/monorail",
"id" => "https://glaceon.social/statuses/123",
"type" => "Note"
})
assert :ok ==
Containment.contain_origin("https://glaceon.social/statuses/123", %{
"actor" => "https://glaceon.social/users/monorail",
"id" => "https://glaceon.social/statuses/123",
"type" => "Note"
})
assert :ok ==
Containment.contain_origin("https://glaceon.social/statuses/123", %{
"attributedTo" => "https://glaceon.social/users/monorail",
"id" => "https://glaceon.social/statuses/123",
"type" => "Note"
})
end
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"})
})
assert capture_log(fn ->
{:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
end) =~
"[error] Could not decode user at fetch https://n1u.moe/users/rye"
end
test "contain_origin_from_id() gracefully handles cases where no ID is present" do
data = %{
"type" => "Create",
"object" => %{
"id" => "http://example.net/~alyssa/activities/1234",
"attributedTo" => "http://example.org/~alyssa"
},
"actor" => "http://example.com/~bob"
}
:error =
Containment.contain_origin_from_id("http://example.net/~alyssa/activities/1234", data)
end
end
describe "containment of children" do
test "contain_child() catches spoofing attempts" do
data = %{
"id" => "http://example.com/whatever",
"type" => "Create",
"object" => %{
"id" => "http://example.net/~alyssa/activities/1234",
"attributedTo" => "http://example.org/~alyssa"
},
"actor" => "http://example.com/~bob"
}
:error = Containment.contain_child(data)
end
test "contain_child() allows correct origins" do
data = %{
"id" => "http://example.org/~alyssa/activities/5678",
"type" => "Create",
"object" => %{
"id" => "http://example.org/~alyssa/activities/1234",
"attributedTo" => "http://example.org/~alyssa"
},
"actor" => "http://example.org/~alyssa"
}
:ok = Containment.contain_child(data)
end
end
end