forked from mirrors/misskey
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:
parent
408e94f41f
commit
6c40c96369
2 changed files with 108 additions and 18 deletions
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)})`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue