diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index 3dbad8a97..f6d33b854 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -42,8 +42,8 @@ export default async (user: { id: User["id"] }, url: string, object: any) => { export async function apGet( url: string, user?: ILocalUser, - redirects: boolean = true -): Promise { + redirects: boolean = true, +): Promise<{ finalUrl: string; content: IObject }> { if (!isValidUrl(url)) { throw new StatusError("Invalid URL", 400); } @@ -72,7 +72,8 @@ export async function apGet( if (redirects && [301, 302, 307, 308].includes(res.status)) { const newUrl = res.headers.get("location"); - if (newUrl == null) throw new Error("apGet got redirect but no target location"); + if (newUrl == null) + throw new Error("apGet got redirect but no target location"); apLogger.debug(`apGet is redirecting to ${newUrl}`); return apGet(newUrl, user, false); } @@ -90,7 +91,8 @@ export async function apGet( if (redirects && [301, 302, 307, 308].includes(res.status)) { const newUrl = res.headers.get("location"); - if (newUrl == null) throw new Error("apGet got redirect but no target location"); + if (newUrl == null) + throw new Error("apGet got redirect but no target location"); apLogger.debug(`apGet is redirecting to ${newUrl}`); return apGet(newUrl, undefined, false); } @@ -98,7 +100,9 @@ export async function apGet( const contentType = res.headers.get("content-type"); if (contentType == null || !validateContentType(contentType)) { - throw new Error(`apGet response had unexpected content-type: ${contentType}`); + throw new Error( + `apGet response had unexpected content-type: ${contentType}`, + ); } if (res.body == null) throw new Error("body is null"); @@ -106,7 +110,10 @@ export async function apGet( const text = await res.text(); if (text.length > 65536) throw new Error("too big result"); - return JSON.parse(text) as IObject; + return { + finalUrl: res.url, + content: JSON.parse(text) as IObject, + }; } function validateContentType(contentType: string): boolean { diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 19d24d9c3..5db7971a5 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -6,7 +6,13 @@ import { extractHost, isSelfHost } from "backend-rs"; import { apGet } from "./request.js"; import type { IObject, ICollection, IOrderedCollection } from "./type.js"; import { isCollectionOrOrderedCollection, getApId } from "./type.js"; -import { FollowRequests, Notes, NoteReactions, Polls, Users } from "@/models/index.js"; +import { + FollowRequests, + Notes, + NoteReactions, + Polls, + Users, +} from "@/models/index.js"; import { parseUri } from "./db-resolver.js"; import renderNote from "@/remote/activitypub/renderer/note.js"; import { renderLike } from "@/remote/activitypub/renderer/like.js"; @@ -114,7 +120,7 @@ export default class Resolver { apLogger.debug("Getting object from remote, authenticated as user:"); apLogger.debug(JSON.stringify(this.user, null, 2)); - const object = await apGet(value, this.user); + const { finalUrl, content: object } = await apGet(value, this.user); if ( object == null || @@ -127,6 +133,13 @@ export default class Resolver { throw new Error("invalid response"); } + if ( + object.id != null && + new URL(finalUrl).host != new URL(object.id).host + ) { + throw new Error("Object ID host doesn't match final url host"); + } + return object; } @@ -160,7 +173,8 @@ export default class Resolver { // if rest is a if (parsed.rest != null && /^\w+$/.test(parsed.rest)) { const [follower, followee] = await Promise.all( - [parsed.id, parsed.rest].map((id) => Users.findOneByOrFail({ id }))); + [parsed.id, parsed.rest].map((id) => Users.findOneByOrFail({ id })), + ); return renderActivity(renderFollow(follower, followee, url)); }