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>
This commit is contained in:
Julia Johannesen 2026-05-20 09:02:25 -04:00 committed by GitHub
commit 6c40c96369
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 108 additions and 18 deletions

View file

@ -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<string, RemoteDocument> = 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<RemoteDocument> => {
@ -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;
};
}

View file

@ -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)})`);