Commit 6ca70981 authored by rinpatch's avatar rinpatch
Browse files

Fix object spoofing vulnerability in attachments

Validate the content-type of the response when fetching an object,
according to https://www.w3.org/TR/activitypub/#x3-2-retrieving-objects.

content-type headers had to be added to many mocks in order to support
this, some of this was done with a regex. While I did go over the
resulting files to check I didn't modify anything unrelated, there is a
 possibility I missed something.

Closes #1948
parent 99bc175f
Pipeline #32805 passed with stages
in 67 minutes and 49 seconds
......@@ -232,8 +232,24 @@ defp get_object_http(id) do
|> sign_fetch(id, date)
case HTTP.get(id, headers) do
{:ok, %{body: body, status: code}} when code in 200..299 ->
{:ok, body}
{:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
case List.keyfind(headers, "content-type", 0) do
{_, content_type} ->
case Plug.Conn.Utils.media_type(content_type) do
{:ok, "application", "activity+json", _} ->
{:ok, body}
{:ok, "application", "ld+json",
%{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
{:ok, body}
_ ->
{:error, {:content_type, content_type}}
end
_ ->
{:error, {:content_type, nil}}
end
{:ok, %{status: code}} when code in [404, 410] ->
{:error, "Object has been deleted"}
......
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://patch.cx/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"actor": "https://patch.cx/users/rin",
"attachment": [],
"attributedTo": "https://patch.cx/users/rin",
"cc": [
"https://patch.cx/users/rin/followers"
],
"content": "Oracle Corporation (NYSE: ORCL) today announced that it has signed a definitive merger agreement to acquire Pleroma AG (FRA: PLA), for $26.50 per share (approximately $10.3 billion). The transaction has been approved by the boards of directors of both companies and should close by early January.",
"context": "https://patch.cx/contexts/spoof",
"id": "https://patch.cx/objects/spoof",
"published": "2020-10-23T18:02:06.038856Z",
"sensitive": false,
"summary": "Oracle buys Pleroma",
"tag": [],
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Note"
}
......@@ -21,6 +21,17 @@ defmodule Pleroma.Object.FetcherTest do
%{method: :get, url: "https://mastodon.example.org/users/userisgone404"} ->
%Tesla.Env{status: 404}
%{
method: :get,
url:
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/json"}],
body: File.read!("test/fixtures/spoofed-object.json")
}
env ->
apply(HttpRequestMock, :request, [env])
end)
......@@ -34,19 +45,22 @@ defmodule Pleroma.Object.FetcherTest do
%{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json")
body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
%{method: :get, url: "https://social.sakamoto.gq/users/eal"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/fetch_mocks/eal.json")
body: File.read!("test/fixtures/fetch_mocks/eal.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
%{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json")
body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
%{method: :get, url: "https://busshi.moe/users/tuxcrafting"} ->
......@@ -132,6 +146,13 @@ test "Return MRF reason when fetched status is rejected by one" do
"http://mastodon.example.org/@admin/99541947525187367"
)
end
test "it does not fetch a spoofed object uploaded on an instance as an attachment" do
assert {:error, _} =
Fetcher.fetch_object_from_id(
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
)
end
end
describe "implementation quirks" do
......
......@@ -281,7 +281,11 @@ test "does not fetch unknown objects when fetch_remote is false" do
setup do
mock(fn
%{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")}
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
env ->
apply(HttpRequestMock, :request, [env])
......@@ -315,7 +319,8 @@ test "refetches if the time since the last refetch is greater than the interval"
mock_modified.(%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
headers: HttpRequestMock.activitypub_object_headers()
})
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
......@@ -359,7 +364,8 @@ test "does not refetch if the time since the last refetch is greater than the in
mock_modified.(%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
headers: HttpRequestMock.activitypub_object_headers()
})
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
......@@ -387,7 +393,8 @@ test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
mock_modified.(%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
headers: HttpRequestMock.activitypub_object_headers()
})
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
......
......@@ -1426,19 +1426,25 @@ test "doesn't crash when follower and following counters are hidden" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_hidden_counters/following" ->
json(%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://localhost:4001/users/masto_hidden_counters/followers"
})
json(
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://localhost:4001/users/masto_hidden_counters/followers"
},
headers: HttpRequestMock.activitypub_object_headers()
)
"http://localhost:4001/users/masto_hidden_counters/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
"http://localhost:4001/users/masto_hidden_counters/followers" ->
json(%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://localhost:4001/users/masto_hidden_counters/following"
})
json(
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://localhost:4001/users/masto_hidden_counters/following"
},
headers: HttpRequestMock.activitypub_object_headers()
)
"http://localhost:4001/users/masto_hidden_counters/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
......@@ -2278,7 +2284,7 @@ test "allow fetching of accounts with an empty string name field" do
Tesla.Mock.mock(fn
%{method: :get, url: "https://princess.cat/users/mewmew"} ->
file = File.read!("test/fixtures/mewmew_no_name.json")
%Tesla.Env{status: 200, body: file}
%Tesla.Env{status: 200, body: file, headers: HttpRequestMock.activitypub_object_headers()}
end)
{:ok, user} = ActivityPub.make_user_from_ap_id("https://princess.cat/users/mewmew")
......
......@@ -60,7 +60,11 @@ test "it works for incoming announces, fetching the announced object" do
Tesla.Mock.mock(fn
%{method: :get} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/mastodon-note-object.json")}
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/mastodon-note-object.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
end)
_user = insert(:user, local: false, ap_id: data["actor"])
......
......@@ -13,7 +13,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.ArticleHandlingTest do
test "Pterotype (Wordpress Plugin) Article" do
Tesla.Mock.mock(fn %{url: "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json")}
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
end)
data =
......@@ -36,13 +40,15 @@ test "Plume Article" do
%{url: "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json")
body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
%{url: "https://baptiste.gelez.xyz/@/BaptisteGelez"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json")
body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
end)
......@@ -61,7 +67,8 @@ test "Prismo Article" do
Tesla.Mock.mock(fn %{url: "https://prismo.news/@mxb"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json")
body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
end)
......
......@@ -48,7 +48,8 @@ test "Funkwhale Audio object" do
%{url: "https://channels.tests.funkwhale.audio/federation/actors/compositions"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json")
body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
end)
......
......@@ -13,13 +13,15 @@ test "Mobilizon Event object" do
%{url: "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json")
body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
%{url: "https://mobilizon.org/@tcit"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json")
body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json"),
headers: HttpRequestMock.activitypub_object_headers()
}
end)
......
......@@ -5,6 +5,8 @@
defmodule HttpRequestMock do
require Logger
def activitypub_object_headers, do: [{"content-type", "application/activity+json"}]
def request(
%Tesla.Env{
url: url,
......@@ -34,7 +36,8 @@ def get("https://osada.macgirvin.com/channel/mike", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json")
body: File.read!("test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json"),
headers: activitypub_object_headers()
}}
end
......@@ -42,7 +45,8 @@ def get("https://shitposter.club/users/moonman", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/moonman@shitposter.club.json")
body: File.read!("test/fixtures/tesla_mock/moonman@shitposter.club.json"),
headers: activitypub_object_headers()
}}
end
......@@ -50,7 +54,8 @@ def get("https://mastodon.social/users/emelie/statuses/101849165031453009", _, _
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/status.emelie.json")
body: File.read!("test/fixtures/tesla_mock/status.emelie.json"),
headers: activitypub_object_headers()
}}
end
......@@ -66,7 +71,8 @@ def get("https://mastodon.social/users/emelie", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/emelie.json")
body: File.read!("test/fixtures/tesla_mock/emelie.json"),
headers: activitypub_object_headers()
}}
end
......@@ -78,7 +84,8 @@ def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/rinpatch.json")
body: File.read!("test/fixtures/tesla_mock/rinpatch.json"),
headers: activitypub_object_headers()
}}
end
......@@ -86,7 +93,8 @@ def get("https://patch.cx/objects/tesla_mock/poll_attachment", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/poll_attachment.json")
body: File.read!("test/fixtures/tesla_mock/poll_attachment.json"),
headers: activitypub_object_headers()
}}
end
......@@ -99,7 +107,8 @@ def get(
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/webfinger_emelie.json")
body: File.read!("test/fixtures/tesla_mock/webfinger_emelie.json"),
headers: activitypub_object_headers()
}}
end
......@@ -112,7 +121,8 @@ def get(
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mike@osada.macgirvin.com.json")
body: File.read!("test/fixtures/tesla_mock/mike@osada.macgirvin.com.json"),
headers: activitypub_object_headers()
}}
end
......@@ -190,7 +200,8 @@ def get(
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/lucifermysticus.json")
body: File.read!("test/fixtures/tesla_mock/lucifermysticus.json"),
headers: activitypub_object_headers()
}}
end
......@@ -198,7 +209,8 @@ def get("https://prismo.news/@mxb", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json")
body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json"),
headers: activitypub_object_headers()
}}
end
......@@ -211,7 +223,8 @@ def get(
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/kaniini@hubzilla.example.org.json")
body: File.read!("test/fixtures/tesla_mock/kaniini@hubzilla.example.org.json"),
headers: activitypub_object_headers()
}}
end
......@@ -219,7 +232,8 @@ def get("https://niu.moe/users/rye", _, _, [{"accept", "application/activity+jso
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/rye.json")
body: File.read!("test/fixtures/tesla_mock/rye.json"),
headers: activitypub_object_headers()
}}
end
......@@ -227,7 +241,8 @@ def get("https://n1u.moe/users/rye", _, _, [{"accept", "application/activity+jso
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/rye.json")
body: File.read!("test/fixtures/tesla_mock/rye.json"),
headers: activitypub_object_headers()
}}
end
......@@ -246,7 +261,8 @@ def get("https://puckipedia.com/", _, _, [{"accept", "application/activity+json"
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/puckipedia.com.json")
body: File.read!("test/fixtures/tesla_mock/puckipedia.com.json"),
headers: activitypub_object_headers()
}}
end
......@@ -254,7 +270,8 @@ def get("https://peertube.moe/accounts/7even", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/7even.json")
body: File.read!("test/fixtures/tesla_mock/7even.json"),
headers: activitypub_object_headers()
}}
end
......@@ -262,7 +279,8 @@ def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/peertube.moe-vid.json")
body: File.read!("test/fixtures/tesla_mock/peertube.moe-vid.json"),
headers: activitypub_object_headers()
}}
end
......@@ -270,7 +288,8 @@ def get("https://framatube.org/accounts/framasoft", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json")
body: File.read!("test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json"),
headers: activitypub_object_headers()
}}
end
......@@ -278,7 +297,8 @@ def get("https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/framatube.org-video.json")
body: File.read!("test/fixtures/tesla_mock/framatube.org-video.json"),
headers: activitypub_object_headers()
}}
end
......@@ -286,7 +306,8 @@ def get("https://peertube.social/accounts/craigmaloney", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/craigmaloney.json")
body: File.read!("test/fixtures/tesla_mock/craigmaloney.json"),
headers: activitypub_object_headers()
}}
end
......@@ -294,7 +315,8 @@ def get("https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/peertube-social.json")
body: File.read!("test/fixtures/tesla_mock/peertube-social.json"),
headers: activitypub_object_headers()
}}
end
......@@ -304,7 +326,8 @@ def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _,
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json")
body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json"),
headers: activitypub_object_headers()
}}
end
......@@ -312,7 +335,8 @@ def get("https://mobilizon.org/@tcit", _, _, [{"accept", "application/activity+j
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json")
body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json"),
headers: activitypub_object_headers()
}}
end
......@@ -320,7 +344,8 @@ def get("https://baptiste.gelez.xyz/@/BaptisteGelez", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json")
body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json"),
headers: activitypub_object_headers()
}}
end
......@@ -328,7 +353,8 @@ def get("https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json")
body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json"),
headers: activitypub_object_headers()
}}
end
......@@ -336,7 +362,8 @@ def get("https://wedistribute.org/wp-json/pterotype/v1/object/85810", _, _, _) d
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/wedistribute-article.json")
body: File.read!("test/fixtures/tesla_mock/wedistribute-article.json"),
headers: activitypub_object_headers()
}}
end
......@@ -344,7 +371,8 @@ def get("https://wedistribute.org/wp-json/pterotype/v1/actor/-blog", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json")
body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json"),
headers: activitypub_object_headers()
}}
end
......@@ -352,7 +380,8 @@ def get("http://mastodon.example.org/users/admin", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/admin@mastdon.example.org.json")
body: File.read!("test/fixtures/tesla_mock/admin@mastdon.example.org.json"),
headers: activitypub_object_headers()
}}
end
......@@ -362,7 +391,8 @@ def get("http://mastodon.example.org/users/relay", _, _, [
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/relay@mastdon.example.org.json")
body: File.read!("test/fixtures/tesla_mock/relay@mastdon.example.org.json"),
headers: activitypub_object_headers()
}}
end
......@@ -482,7 +512,8 @@ def get(
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/pekorino@pawoo.net_host_meta.json")
body: File.read!("test/fixtures/tesla_mock/pekorino@pawoo.net_host_meta.json"),
headers: activitypub_object_headers()
}}
end
......@@ -543,7 +574,8 @@ def get(
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/mastodon-note-object.json")
body: File.read!("test/fixtures/mastodon-note-object.json"),
headers: activitypub_object_headers()
}}
end
......@@ -567,7 +599,8 @@ def get("https://mstdn.io/users/mayuutann", _, _, [{"accept", "application/activ
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mayumayu.json")
body: File.read!("test/fixtures/tesla_mock/mayumayu.json"),
headers: activitypub_object_headers()
}}
end
......@@ -580,7 +613,8 @@ def get(
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mayumayupost.json")
body: File.read!("test/fixtures/tesla_mock/mayumayupost.json"),
headers: activitypub_object_headers()
}}
end
......@@ -795,7 +829,8 @@ def get(
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/winterdienst_webfinger.json")
body: File.read!("test/fixtures/tesla_mock/winterdienst_webfinger.json"),
headers: activitypub_object_headers()
}}
end
......@@ -867,12 +902,21 @@ def get("https://social.heldscal.la/.well-known/host-meta", _, _, _) do
end
def get("https://mastodon.social/users/lambadalambda", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.json")}}
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/lambadalambda.json"),
headers: activitypub_object_headers()