From 6c40c96369a9232ea07949cd41d6f98962e5e02e Mon Sep 17 00:00:00 2001 From: Julia Johannesen Date: Wed, 20 May 2026 09:02:25 -0400 Subject: [PATCH] Merge commit from fork * fix: Prevent timing attacks and RDF-graph rewrites * fix: Proper vuln fix, not a bandaid * fix: Accidental removal * fix: Explicitly check for null * fix: Address issues * clean up * lint fixes * fix: reset pnpm-lock.yaml to current develop --------- Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> --- .../src/core/activitypub/JsonLdService.ts | 86 +++++++++++++++++-- .../queue/processors/InboxProcessorService.ts | 40 ++++++--- 2 files changed, 108 insertions(+), 18 deletions(-) diff --git a/packages/backend/src/core/activitypub/JsonLdService.ts b/packages/backend/src/core/activitypub/JsonLdService.ts index 7c7dc01510..d86b9ec496 100644 --- a/packages/backend/src/core/activitypub/JsonLdService.ts +++ b/packages/backend/src/core/activitypub/JsonLdService.ts @@ -9,6 +9,7 @@ import { Injectable } from '@nestjs/common'; import { RsaKeyPair } from 'slacc'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js'; import { validateContentTypeSetAsJsonLD } from './misc/validator.js'; import type { JsonLdDocument } from 'jsonld'; @@ -16,7 +17,40 @@ import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec. // RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017 -class JsonLd { +export class JsonLdError extends IdentifiableError { + constructor(id: string, message?: string) { + super(id, message); + } +} + +export class JsonLdCacheOverflowError extends JsonLdError { + constructor() { + super('42fb039c-69fb-4f75-8187-d3aee412423e', 'context cache overflow'); + } +} + +export class JsonLdCacheFrozenError extends JsonLdError { + constructor() { + super('202c41fa-72d5-4e22-95af-94a8ac83346f', 'attempt to insert into frozen context cache'); + } +} + +export class JsonLdForbiddenDriectiveError extends JsonLdError { + constructor(public directive: string) { + super('0297f79b-0ed9-4b6c-875f-b0a82ff96781', `${directive} is forbidden by Misskey in ActivityPub documents`); + } +} + +export class JsonLd { + private static forbiddenDirectives = new Set([ + '@included', + '@graph', + '@reverse', + ]); + + private frozen = false; + private cache: Map = new Map(); + public debug = false; public preLoad = true; public loderTimeout = 5000; @@ -81,9 +115,9 @@ class JsonLd { const optionsHash = this.sha256(canonizedOptions.toString()); const transformedData = { ...data }; delete transformedData['signature']; - const cannonidedData = await this.normalize(transformedData); - if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`); - const documentHash = this.sha256(cannonidedData.toString()); + const cannonizedData = await this.normalize(transformedData); + if (this.debug) console.debug(`cannonizedData: ${cannonizedData}`); + const documentHash = this.sha256(cannonizedData.toString()); const verifyData = `${optionsHash}${documentHash}`; return verifyData; } @@ -106,6 +140,34 @@ class JsonLd { }); } + /** + * Prevent any further HTTP requests from being made for the sake of + * validating JSON-LD signatures. + */ + @bindThis + public freeze(): void { this.frozen = true; } + + @bindThis + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public checkForForbiddenDirectives(value: any): void { + if (typeof value === 'object' && value !== null) { + if (Array.isArray(value)) { + for (const item of value) this.checkForForbiddenDirectives(item); + } else { + const object = value; + for (const [key, value] of Object.entries(object)) { + if (JsonLd.forbiddenDirectives.has(key)) { + throw new JsonLdForbiddenDriectiveError(key); + } + + if (typeof value === 'object' && value !== null) { + this.checkForForbiddenDirectives(value); + } + } + } + } + } + @bindThis private getLoader() { return async (url: string): Promise => { @@ -122,13 +184,27 @@ class JsonLd { } } + const cached = this.cache.get(url); + if (cached) { + if (this.debug) console.debug(`HIT: ${url}`); + return cached; + } + if (this.debug) console.debug(`MISS: ${url}`); + + if (this.frozen) throw new JsonLdCacheFrozenError(); + const document = await this.fetchDocument(url); - return { + this.checkForForbiddenDirectives(document); + + const remoteDocument = { contextUrl: undefined, document: document, documentUrl: url, }; + this.cache.set(url, remoteDocument); + if (this.cache.size > 256) throw new JsonLdCacheOverflowError(); + return remoteDocument; }; } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 0e4cd59888..b7557ddb6f 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -21,7 +21,7 @@ import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; -import { JsonLdService } from '@/core/activitypub/JsonLdService.js'; +import { JsonLdError, JsonLdService } from '@/core/activitypub/JsonLdService.js'; import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; import { bindThis } from '@/decorators.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; @@ -163,22 +163,17 @@ export class InboxProcessorService implements OnApplicationShutdown { const jsonLd = this.jsonLdService.use(); - // LD-Signature検証 - const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); - if (!verified) { - throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); - } - - // アクティビティを正規化 delete activity.signature; try { activity = await jsonLd.compact(activity) as IActivity; - } catch (e) { - throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`); + } catch (error) { + throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${error}`); + } + try { + jsonLd.checkForForbiddenDirectives(activity); + } catch (error) { + throw new Bull.UnrecoverableError(`skip: ${error}`); } - // TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする - // https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29 - activity.signature = ldSignature; //#region Log const compactedInfo = Object.assign({}, activity); @@ -186,6 +181,25 @@ export class InboxProcessorService implements OnApplicationShutdown { this.logger.debug(`compacted: ${JSON.stringify(compactedInfo, null, 2)}`); //#endregion + activity.signature = ldSignature; + + jsonLd.freeze(); + + // LD-Signature検証 + let verified; + try { + verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem); + if (!verified) { + throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); + } + } catch (error) { + if (error instanceof JsonLdError) { + throw new Bull.UnrecoverableError(`skip: encountered a JSON-LD error while verifying signature: ${error}`); + } else { + throw error; + } + } + // もう一度actorチェック if (authUser.user.uri !== getApId(activity.actor)) { throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${getApId(activity.actor)})`);