diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index 688b2c953..a7404e603 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -54,6 +54,39 @@ export async function request(user: { id: User['id'] }, url: string, object: any } } +// Determine that the content type actually is an activitypub object. +// +// This defends against the possibility of a user of a remote instance uploading +// something that looks like an ActivityPub object and thus masquerading as any +// other user on that same instance. It in turn depends on the server not +// returning that content as an ActivityPub MIME type. +// +// Ref: GHSA-jhrq-qvrm-qr36 +function isActivitypub(_contentType: string): boolean { + const contentType = _contentType.toLowerCase() + if (contentType.startsWith('application/activity+json')) { + return true; + } + if (contentType.startsWith('application/ld+json')) { + // oh lord, actually parsing the MIME type + // Ref: § 5.1 + // Ref: + let start = contentType.indexOf('profile="'); + if (start === -1) return false; // profile is required for our purposes + + start += 'profile="'.length; + let end = contentType.indexOf('"', start); + if (end === -1) return false; // malformed MIME type + + let profiles = contentType.substring(start, end).split(/\s+/); + if (profiles.includes('https://www.w3.org/ns/activitystreams')) { + return true; + } + } + + return false; +}; + /** * Get AP object with http-signature * @param user http-signature user @@ -87,39 +120,6 @@ export async function signedGet(_url: string, user: { id: User['id'] }): Promise // Use Location header and fetched URL as the base URL. url = new URL(res.headers.get('Location'), url).href; } else { - // Determine that the content type we got back actually is an activitypub object. - // - // This defends against the possibility of a user of a remote instance uploading - // something that looks like an ActivityPub object and thus masquerading as any - // other user on that same instance. It in turn depends on the server not - // returning that content as an ActivityPub MIME type. - // - // Ref: GHSA-jhrq-qvrm-qr36 - const isActivitypub = (_contentType) => { - const contentType = _contentType.toLowerCase() - if (contentType.startsWith('application/activity+json')) { - return true; - } - if (contentType.startsWith('application/ld+json')) { - // oh lord, actually parsing the MIME type - // Ref: § 5.1 - // Ref: - let start = contentType.indexOf('profile="'); - if (start === -1) return false; // profile is required for our purposes - - start += 'profile="'.length; - let end = contentType.indexOf('"', start); - if (end === -1) return false; // malformed MIME type - - let profiles = contentType.substring(start, end).split(/\s+/); - if (profiles.includes('https://www.w3.org/ns/activitystreams')) { - return true; - } - } - - return false; - }; - if (!isActivitypub(res.headers.get('Content-Type'))) { throw new Error('invalid response content type'); }