fix (backend): verify object id host matches final URL when fetching remote activities

5f6096c1b7

Co-authored-by: naskya <m@naskya.net>
This commit is contained in:
Laura Hausmann 2024-03-27 07:16:44 +09:00 committed by naskya
parent 850c52ef63
commit ada0137a35
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
2 changed files with 30 additions and 9 deletions

View file

@ -42,8 +42,8 @@ export default async (user: { id: User["id"] }, url: string, object: any) => {
export async function apGet( export async function apGet(
url: string, url: string,
user?: ILocalUser, user?: ILocalUser,
redirects: boolean = true redirects: boolean = true,
): Promise<IObject> { ): Promise<{ finalUrl: string; content: IObject }> {
if (!isValidUrl(url)) { if (!isValidUrl(url)) {
throw new StatusError("Invalid URL", 400); throw new StatusError("Invalid URL", 400);
} }
@ -72,7 +72,8 @@ export async function apGet(
if (redirects && [301, 302, 307, 308].includes(res.status)) { if (redirects && [301, 302, 307, 308].includes(res.status)) {
const newUrl = res.headers.get("location"); 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}`); apLogger.debug(`apGet is redirecting to ${newUrl}`);
return apGet(newUrl, user, false); return apGet(newUrl, user, false);
} }
@ -90,7 +91,8 @@ export async function apGet(
if (redirects && [301, 302, 307, 308].includes(res.status)) { if (redirects && [301, 302, 307, 308].includes(res.status)) {
const newUrl = res.headers.get("location"); 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}`); apLogger.debug(`apGet is redirecting to ${newUrl}`);
return apGet(newUrl, undefined, false); return apGet(newUrl, undefined, false);
} }
@ -98,7 +100,9 @@ export async function apGet(
const contentType = res.headers.get("content-type"); const contentType = res.headers.get("content-type");
if (contentType == null || !validateContentType(contentType)) { 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"); if (res.body == null) throw new Error("body is null");
@ -106,7 +110,10 @@ export async function apGet(
const text = await res.text(); const text = await res.text();
if (text.length > 65536) throw new Error("too big result"); 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 { function validateContentType(contentType: string): boolean {

View file

@ -6,7 +6,13 @@ import { extractDbHost, isSelfHost } from "@/misc/convert-host.js";
import { apGet } from "./request.js"; import { apGet } from "./request.js";
import type { IObject, ICollection, IOrderedCollection } from "./type.js"; import type { IObject, ICollection, IOrderedCollection } from "./type.js";
import { isCollectionOrOrderedCollection, getApId } 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 { parseUri } from "./db-resolver.js";
import renderNote from "@/remote/activitypub/renderer/note.js"; import renderNote from "@/remote/activitypub/renderer/note.js";
import { renderLike } from "@/remote/activitypub/renderer/like.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("Getting object from remote, authenticated as user:");
apLogger.debug(JSON.stringify(this.user, null, 2)); 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 ( if (
object == null || object == null ||
@ -127,6 +133,13 @@ export default class Resolver {
throw new Error("invalid response"); 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; return object;
} }
@ -160,7 +173,8 @@ export default class Resolver {
// if rest is a <followee id> // if rest is a <followee id>
if (parsed.rest != null && /^\w+$/.test(parsed.rest)) { if (parsed.rest != null && /^\w+$/.test(parsed.rest)) {
const [follower, followee] = await Promise.all( 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)); return renderActivity(renderFollow(follower, followee, url));
} }