diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 967c4762fb..c5ac8a944d 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -23,7 +23,7 @@ import { getApId, getNullableApId, isCollectionOrOrderedCollection } from './typ import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; -import type { IObject, ICollection, IOrderedCollection } from './type.js'; +import type { IObject, ICollection, IOrderedCollection, ApObject } from './type.js'; export class Resolver { private history: Set; @@ -76,6 +76,33 @@ export class Resolver { } } + /** + * Securely resolves an AP object or URL that has been sent from another instance. + * An input object is trusted if and only if its ID matches the authority of sentFromUri. + * In all other cases, the object is re-fetched from remote by input string or object ID. + */ + @bindThis + public async secureResolve(input: ApObject, sentFromUri: string): Promise { + // Unpack arrays to get the value element. + const value = fromTuple(input); + if (value == null) { + throw new IdentifiableError('20058164-9de1-4573-8715-425753a21c1d', 'Cannot resolve null input'); + } + + // This will throw if the input has no ID, which is good because we can't verify an anonymous object anyway. + const id = getApId(value); + + // Check if we can use the provided object as-is. + // Our security requires that the object ID matches the host authority that sent it, otherwise it can't be trusted. + // A mismatch isn't necessarily malicious, it just means we can't use the object we were given. + if (typeof(value) === 'object' && this.apUtilityService.haveSameAuthority(id, sentFromUri)) { + return value; + } + + // If the checks didn't pass, then we must fetch the object and use that. + return await this.resolve(id); + } + @bindThis public async resolve(value: string | IObject | [string | IObject]): Promise { // eslint-disable-next-line no-param-reassign diff --git a/packages/backend/src/misc/from-tuple.ts b/packages/backend/src/misc/from-tuple.ts index 366b1e310f..034bae584b 100644 --- a/packages/backend/src/misc/from-tuple.ts +++ b/packages/backend/src/misc/from-tuple.ts @@ -1,4 +1,11 @@ -export function fromTuple(value: T | [T]): T { +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function fromTuple(value: T | [T]): T; +export function fromTuple(value: T | [T] | T[]): T | undefined; +export function fromTuple(value: T | [T] | T[]): T | undefined { if (Array.isArray(value)) { return value[0]; }