check content-type header on AP requests

Changelog: Security
Ref: GHSA-jhrq-qvrm-qr36
This commit is contained in:
Johann150 2024-02-17 09:04:36 +01:00
parent c8f8e4c01d
commit 47b3277201
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1

View file

@ -88,7 +88,42 @@ 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 {
return await res.json();
// 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: <urn:ietf:rfc:2045> § 5.1
// Ref: <https://www.iana.org/assignments/media-types/application/ld+json>
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'))) {
return await res.json();
}
}
}