Merge commit from fork
* fix(backend): Fix an issue where the origin of ActivityPub lookup response was not validated correctly. [GHSA-6w2c-vf6f-xf26](https://github.com/misskey-dev/misskey/security/advisories/GHSA-6w2c-vf6f-xf26) Signed-off-by: eternal-flame-AD <yume@yumechi.jp> * Enhance: Add configuration option to disable all external redirects when responding to an ActivityPub lookup (config.disallowExternalApRedirect) Signed-off-by: eternal-flame-AD <yume@yumechi.jp> * fixup! fix(backend): Fix an issue where the origin of ActivityPub lookup response was not validated correctly. * docs & one edge case Signed-off-by: eternal-flame-AD <yume@yumechi.jp> * apply suggestions Signed-off-by: eternal-flame-AD <yume@yumechi.jp> * remove stale frontend reference to _responseInvalidIdHostNotMatch Signed-off-by: eternal-flame-AD <yume@yumechi.jp> * apply suggestions Signed-off-by: eternal-flame-AD <yume@yumechi.jp> --------- Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
@@ -103,6 +103,43 @@ export class ServerService implements OnApplicationShutdown {
|
||||
serve: false,
|
||||
});
|
||||
|
||||
// if the requester looks like to be performing an ActivityPub object lookup, reject all external redirects
|
||||
//
|
||||
// this will break lookup that involve copying a URL from a third-party server, like trying to lookup http://charlie.example.com/@alice@alice.com
|
||||
//
|
||||
// this is not required by standard but protect us from peers that did not validate final URL.
|
||||
if (this.config.disallowExternalApRedirect) {
|
||||
const maybeApLookupRegex = /application\/activity\+json|application\/ld\+json.+activitystreams/i;
|
||||
fastify.addHook('onSend', (request, reply, _, done) => {
|
||||
const location = reply.getHeader('location');
|
||||
if (reply.statusCode < 300 || reply.statusCode >= 400 || typeof location !== 'string') {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!maybeApLookupRegex.test(request.headers.accept ?? '')) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveLocation = process.env.NODE_ENV === 'production' ? location : location.replace(/^http:\/\//, 'https://');
|
||||
if (effectiveLocation.startsWith(`https://${this.config.host}/`)) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
reply.status(406);
|
||||
reply.removeHeader('location');
|
||||
reply.header('content-type', 'text/plain; charset=utf-8');
|
||||
reply.header('link', `<${encodeURI(location)}>; rel="canonical"`);
|
||||
done(null, [
|
||||
"Refusing to relay remote ActivityPub object lookup.",
|
||||
"",
|
||||
`Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`,
|
||||
].join('\n'));
|
||||
});
|
||||
}
|
||||
|
||||
fastify.register(this.apiServerService.createServer, { prefix: '/api' });
|
||||
fastify.register(this.openApiServerService.createServer);
|
||||
fastify.register(this.fileServerService.createServer);
|
||||
|
@@ -20,6 +20,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
@@ -53,11 +54,6 @@ export const meta = {
|
||||
code: 'RESPONSE_INVALID',
|
||||
id: '70193c39-54f3-4813-82f0-70a680f7495b',
|
||||
},
|
||||
responseInvalidIdHostNotMatch: {
|
||||
message: 'Requested URI and response URI host does not match.',
|
||||
code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH',
|
||||
id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a',
|
||||
},
|
||||
noSuchObject: {
|
||||
message: 'No such object.',
|
||||
code: 'NO_SUCH_OBJECT',
|
||||
@@ -153,7 +149,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
// リモートから一旦オブジェクトフェッチ
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
const object = await resolver.resolve(uri).catch((err) => {
|
||||
// allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob)
|
||||
const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => {
|
||||
if (err instanceof IdentifiableError) {
|
||||
switch (err.id) {
|
||||
// resolve
|
||||
@@ -165,10 +162,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
|
||||
throw new ApiError(meta.errors.federationNotAllowed);
|
||||
case '72180409-793c-4973-868e-5a118eb5519b':
|
||||
case 'ad2dc287-75c1-44c4-839d-3d2e64576675':
|
||||
throw new ApiError(meta.errors.responseInvalid);
|
||||
case 'fd93c2fa-69a8-440f-880b-bf178e0ec877':
|
||||
throw new ApiError(meta.errors.responseInvalidIdHostNotMatch);
|
||||
|
||||
// resolveLocal
|
||||
case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8':
|
||||
|
Reference in New Issue
Block a user