Skip to content

Commit 6c40c96

Browse files
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>
1 parent 408e94f commit 6c40c96

2 files changed

Lines changed: 108 additions & 18 deletions

File tree

packages/backend/src/core/activitypub/JsonLdService.ts

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,48 @@ import { Injectable } from '@nestjs/common';
99
import { RsaKeyPair } from 'slacc';
1010
import { HttpRequestService } from '@/core/HttpRequestService.js';
1111
import { bindThis } from '@/decorators.js';
12+
import { IdentifiableError } from '@/misc/identifiable-error.js';
1213
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
1314
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
1415
import type { JsonLdDocument } from 'jsonld';
1516
import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js';
1617

1718
// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
1819

19-
class JsonLd {
20+
export class JsonLdError extends IdentifiableError {
21+
constructor(id: string, message?: string) {
22+
super(id, message);
23+
}
24+
}
25+
26+
export class JsonLdCacheOverflowError extends JsonLdError {
27+
constructor() {
28+
super('42fb039c-69fb-4f75-8187-d3aee412423e', 'context cache overflow');
29+
}
30+
}
31+
32+
export class JsonLdCacheFrozenError extends JsonLdError {
33+
constructor() {
34+
super('202c41fa-72d5-4e22-95af-94a8ac83346f', 'attempt to insert into frozen context cache');
35+
}
36+
}
37+
38+
export class JsonLdForbiddenDriectiveError extends JsonLdError {
39+
constructor(public directive: string) {
40+
super('0297f79b-0ed9-4b6c-875f-b0a82ff96781', `${directive} is forbidden by Misskey in ActivityPub documents`);
41+
}
42+
}
43+
44+
export class JsonLd {
45+
private static forbiddenDirectives = new Set([
46+
'@included',
47+
'@graph',
48+
'@reverse',
49+
]);
50+
51+
private frozen = false;
52+
private cache: Map<string, RemoteDocument> = new Map();
53+
2054
public debug = false;
2155
public preLoad = true;
2256
public loderTimeout = 5000;
@@ -81,9 +115,9 @@ class JsonLd {
81115
const optionsHash = this.sha256(canonizedOptions.toString());
82116
const transformedData = { ...data };
83117
delete transformedData['signature'];
84-
const cannonidedData = await this.normalize(transformedData);
85-
if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
86-
const documentHash = this.sha256(cannonidedData.toString());
118+
const cannonizedData = await this.normalize(transformedData);
119+
if (this.debug) console.debug(`cannonizedData: ${cannonizedData}`);
120+
const documentHash = this.sha256(cannonizedData.toString());
87121
const verifyData = `${optionsHash}${documentHash}`;
88122
return verifyData;
89123
}
@@ -106,6 +140,34 @@ class JsonLd {
106140
});
107141
}
108142

143+
/**
144+
* Prevent any further HTTP requests from being made for the sake of
145+
* validating JSON-LD signatures.
146+
*/
147+
@bindThis
148+
public freeze(): void { this.frozen = true; }
149+
150+
@bindThis
151+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
152+
public checkForForbiddenDirectives(value: any): void {
153+
if (typeof value === 'object' && value !== null) {
154+
if (Array.isArray(value)) {
155+
for (const item of value) this.checkForForbiddenDirectives(item);
156+
} else {
157+
const object = value;
158+
for (const [key, value] of Object.entries(object)) {
159+
if (JsonLd.forbiddenDirectives.has(key)) {
160+
throw new JsonLdForbiddenDriectiveError(key);
161+
}
162+
163+
if (typeof value === 'object' && value !== null) {
164+
this.checkForForbiddenDirectives(value);
165+
}
166+
}
167+
}
168+
}
169+
}
170+
109171
@bindThis
110172
private getLoader() {
111173
return async (url: string): Promise<RemoteDocument> => {
@@ -122,13 +184,27 @@ class JsonLd {
122184
}
123185
}
124186

187+
const cached = this.cache.get(url);
188+
if (cached) {
189+
if (this.debug) console.debug(`HIT: ${url}`);
190+
return cached;
191+
}
192+
125193
if (this.debug) console.debug(`MISS: ${url}`);
194+
195+
if (this.frozen) throw new JsonLdCacheFrozenError();
196+
126197
const document = await this.fetchDocument(url);
127-
return {
198+
this.checkForForbiddenDirectives(document);
199+
200+
const remoteDocument = {
128201
contextUrl: undefined,
129202
document: document,
130203
documentUrl: url,
131204
};
205+
this.cache.set(url, remoteDocument);
206+
if (this.cache.size > 256) throw new JsonLdCacheOverflowError();
207+
return remoteDocument;
132208
};
133209
}
134210

packages/backend/src/queue/processors/InboxProcessorService.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
2121
import { StatusError } from '@/misc/status-error.js';
2222
import { UtilityService } from '@/core/UtilityService.js';
2323
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
24-
import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
24+
import { JsonLdError, JsonLdService } from '@/core/activitypub/JsonLdService.js';
2525
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
2626
import { bindThis } from '@/decorators.js';
2727
import { IdentifiableError } from '@/misc/identifiable-error.js';
@@ -163,29 +163,43 @@ export class InboxProcessorService implements OnApplicationShutdown {
163163

164164
const jsonLd = this.jsonLdService.use();
165165

166-
// LD-Signature検証
167-
const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
168-
if (!verified) {
169-
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
170-
}
171-
172-
// アクティビティを正規化
173166
delete activity.signature;
174167
try {
175168
activity = await jsonLd.compact(activity) as IActivity;
176-
} catch (e) {
177-
throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`);
169+
} catch (error) {
170+
throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${error}`);
171+
}
172+
try {
173+
jsonLd.checkForForbiddenDirectives(activity);
174+
} catch (error) {
175+
throw new Bull.UnrecoverableError(`skip: ${error}`);
178176
}
179-
// TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする
180-
// https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29
181-
activity.signature = ldSignature;
182177

183178
//#region Log
184179
const compactedInfo = Object.assign({}, activity);
185180
delete compactedInfo['@context'];
186181
this.logger.debug(`compacted: ${JSON.stringify(compactedInfo, null, 2)}`);
187182
//#endregion
188183

184+
activity.signature = ldSignature;
185+
186+
jsonLd.freeze();
187+
188+
// LD-Signature検証
189+
let verified;
190+
try {
191+
verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem);
192+
if (!verified) {
193+
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
194+
}
195+
} catch (error) {
196+
if (error instanceof JsonLdError) {
197+
throw new Bull.UnrecoverableError(`skip: encountered a JSON-LD error while verifying signature: ${error}`);
198+
} else {
199+
throw error;
200+
}
201+
}
202+
189203
// もう一度actorチェック
190204
if (authUser.user.uri !== getApId(activity.actor)) {
191205
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${getApId(activity.actor)})`);

0 commit comments

Comments
 (0)