Skip to content

Instantly share code, notes, and snippets.

@tesaguri
Last active April 30, 2024 08:11
Show Gist options
  • Save tesaguri/f3c73f81bc000f669fc8adfab316603b to your computer and use it in GitHub Desktop.
Save tesaguri/f3c73f81bc000f669fc8adfab316603b to your computer and use it in GitHub Desktop.

The patch file hotfix-ghsa-2vxv-pv3m-3wvj.patch is a hotfix for Misskey v2024.3.1 (misskey-dev/misskey@78ff90f) aimed at fixing a security issue disclosed by its forks around 2024-04-29Z.

See advisory.md for details of the security issue.

Usage

Use the Docker container image

Pull the image with the following command:

$ docker pull ghcr.io/tesaguri/misskey:2024.3.1-hotfix-ghsa-2vxv-pv3m-3wvj

Build from source

Clone the source code of Misskey and apply the patch with the following command:

mkdir misskey &&
	cd misskey &&
	git init &&
	git remote add origin https://github.com/misskey-dev/misskey.git &&
	git fetch origin 2024.3.1 &&
	git checkout FETCH_HEAD &&
	git switch -c hotfix/ghsa-2vxv-pv3m-3wvj &&
	curl -fLOSs https://gist.github.com/tesaguri/f3c73f81bc000f669fc8adfab316603b/raw/hotfix-ghsa-2vxv-pv3m-3wvj.patch &&
	git am hotfix-ghsa-2vxv-pv3m-3wvj.patch

Then, install the source code by following the official installation instruction.

Copyright

To the extent possible under law, the author of this patch (hotfix-ghsa-2vxv-pv3m-3wvj.patch) has waived all copyright and related or neighboring rights to this work. See the UNLICENSE file for the full copyright waiver.

Some portion of the patch file includes a (modified/verbatim) copy of the original Misskey source code, which is distributed under the terms of the GNU Affero General Public License, version 3 (http://www.gnu.org/licenses/), with the copyright notice of Copyright © 2014-2024 syuilo and contributors.

Impersonation and takeover of remote accounts with unnormalized signed activities

Summary

Misskey doesn't perform proper normalization on the JSON structures of incoming signed ActivityPub activity objects before processing them, allowing threat actors to spoof the contents of signed activities and impersonate the authors of the original activities.

Details

The reporter intends to keep this section undisclosed at least for 30 days after the publication of the advisory and until the remedy has been deployed widely.

PoC

The reporter intends to keep this section undisclosed at least for 30 days after the publication of the advisory and until the remedy has been deployed widely.

Impact

The vulnerability allows a threat actor to impersonate a target remote account and perform spoofed activities of any type attributed to the target account, provided that the threat actor has access to a valid Linked Data Signature by the target account.

There are a number of situations where the threat actor can obtain a valid signature by the target account, including:

  • The target account sends a signed activity to another account and the recipient account's server forwards the activity to a server controlled by the threat actor according to the inbox forwarding mechanism of ActivityPub
  • The target account has joined a relay to which a server controlled by the threat actor subscribes, and sends a signed activity to that relay

Estimated severity

CVSS vector string CVSS base score
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N 8.2

Timeline

Date and time Event
2022-02-03 The same kind of vulnerability in Mastodon was disclosed: CVE-2022-24307
2024-04-17T13:25Z The vulnerability report was submitted to misskey-dev
2024-04-26T16:09Z The patch was submitted to misskey-dev
2024-04-29Z PeerTube released v6.1, which fixes the same kind of vulnerability
2024-04-29T14:36Z Iceshrimp released v2023.12.7, which fixes the same vulnerability
2024-04-29T18Z This advisory was published
2024-04-29Z Meisskey released v10.102.699-m544 and v11.37.1-20240430023339, which fix the same vulneravility, and published a security advisory
2024-04-29Z Firefish released v20240430, which fixes the same vulnerability
From 9b9e397ddcc799e77c62cba42e980a2792c193ba Mon Sep 17 00:00:00 2001
From: Daiki Mizukami <[email protected]>
Date: Fri, 26 Apr 2024 21:35:30 +0900
Subject: [PATCH] fix: compact incoming signed activities
---
packages/backend/src/core/CoreModule.ts | 12 ++---
.../src/core/activitypub/ApRendererService.ts | 45 +++----------------
...LdSignatureService.ts => JsonLdService.ts} | 32 ++++++++-----
.../src/core/activitypub/misc/contexts.ts | 39 +++++++++++++++-
.../queue/processors/InboxProcessorService.ts | 44 +++++++++++++-----
5 files changed, 102 insertions(+), 70 deletions(-)
rename packages/backend/src/core/activitypub/{LdSignatureService.ts => JsonLdService.ts} (83%)
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 2c27d33c06..5953155872 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -127,7 +127,7 @@ import { ApMfmService } from './activitypub/ApMfmService.js';
import { ApRendererService } from './activitypub/ApRendererService.js';
import { ApRequestService } from './activitypub/ApRequestService.js';
import { ApResolverService } from './activitypub/ApResolverService.js';
-import { LdSignatureService } from './activitypub/LdSignatureService.js';
+import { JsonLdService } from './activitypub/JsonLdService.js';
import { RemoteLoggerService } from './RemoteLoggerService.js';
import { RemoteUserResolveService } from './RemoteUserResolveService.js';
import { WebfingerService } from './WebfingerService.js';
@@ -266,7 +266,7 @@ const $ApMfmService: Provider = { provide: 'ApMfmService', useExisting: ApMfmSer
const $ApRendererService: Provider = { provide: 'ApRendererService', useExisting: ApRendererService };
const $ApRequestService: Provider = { provide: 'ApRequestService', useExisting: ApRequestService };
const $ApResolverService: Provider = { provide: 'ApResolverService', useExisting: ApResolverService };
-const $LdSignatureService: Provider = { provide: 'LdSignatureService', useExisting: LdSignatureService };
+const $JsonLdService: Provider = { provide: 'JsonLdService', useExisting: JsonLdService };
const $RemoteLoggerService: Provider = { provide: 'RemoteLoggerService', useExisting: RemoteLoggerService };
const $RemoteUserResolveService: Provider = { provide: 'RemoteUserResolveService', useExisting: RemoteUserResolveService };
const $WebfingerService: Provider = { provide: 'WebfingerService', useExisting: WebfingerService };
@@ -406,7 +406,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApRendererService,
ApRequestService,
ApResolverService,
- LdSignatureService,
+ JsonLdService,
RemoteLoggerService,
RemoteUserResolveService,
WebfingerService,
@@ -542,7 +542,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ApRendererService,
$ApRequestService,
$ApResolverService,
- $LdSignatureService,
+ $JsonLdService,
$RemoteLoggerService,
$RemoteUserResolveService,
$WebfingerService,
@@ -678,7 +678,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApRendererService,
ApRequestService,
ApResolverService,
- LdSignatureService,
+ JsonLdService,
RemoteLoggerService,
RemoteUserResolveService,
WebfingerService,
@@ -813,7 +813,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ApRendererService,
$ApRequestService,
$ApResolverService,
- $LdSignatureService,
+ $JsonLdService,
$RemoteLoggerService,
$RemoteUserResolveService,
$WebfingerService,
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index d7fb977a99..d3553b6f73 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -28,8 +28,9 @@ import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js';
-import { LdSignatureService } from './LdSignatureService.js';
+import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
+import { CONTEXT } from './misc/contexts.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
@Injectable()
@@ -56,7 +57,7 @@ export class ApRendererService {
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
- private ldSignatureService: LdSignatureService,
+ private jsonLdService: JsonLdService,
private userKeypairService: UserKeypairService,
private apMfmService: ApMfmService,
private mfmService: MfmService,
@@ -617,48 +618,16 @@ export class ApRendererService {
x.id = `${this.config.url}/${randomUUID()}`;
}
- return Object.assign({
- '@context': [
- 'https://www.w3.org/ns/activitystreams',
- 'https://w3id.org/security/v1',
- {
- Key: 'sec:Key',
- // as non-standards
- manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
- sensitive: 'as:sensitive',
- Hashtag: 'as:Hashtag',
- quoteUrl: 'as:quoteUrl',
- // Mastodon
- toot: 'http://joinmastodon.org/ns#',
- Emoji: 'toot:Emoji',
- featured: 'toot:featured',
- discoverable: 'toot:discoverable',
- // schema
- schema: 'http://schema.org#',
- PropertyValue: 'schema:PropertyValue',
- value: 'schema:value',
- // Misskey
- misskey: 'https://misskey-hub.net/ns#',
- '_misskey_content': 'misskey:_misskey_content',
- '_misskey_quote': 'misskey:_misskey_quote',
- '_misskey_reaction': 'misskey:_misskey_reaction',
- '_misskey_votes': 'misskey:_misskey_votes',
- '_misskey_summary': 'misskey:_misskey_summary',
- 'isCat': 'misskey:isCat',
- // vcard
- vcard: 'http://www.w3.org/2006/vcard/ns#',
- },
- ],
- }, x as T & { id: string });
+ return Object.assign({ '@context': CONTEXT }, x as T & { id: string });
}
@bindThis
public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> {
const keypair = await this.userKeypairService.getUserKeypair(user.id);
- const ldSignature = this.ldSignatureService.use();
- ldSignature.debug = false;
- activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
+ const jsonLd = this.jsonLdService.use();
+ jsonLd.debug = false;
+ activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
return activity;
}
diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/JsonLdService.ts
similarity index 83%
rename from packages/backend/src/core/activitypub/LdSignatureService.ts
rename to packages/backend/src/core/activitypub/JsonLdService.ts
index 9de184336f..100d4fa19f 100644
--- a/packages/backend/src/core/activitypub/LdSignatureService.ts
+++ b/packages/backend/src/core/activitypub/JsonLdService.ts
@@ -7,14 +7,14 @@ import * as crypto from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
-import { CONTEXTS } from './misc/contexts.js';
+import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
import type { JsonLdDocument } from 'jsonld';
-import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
+import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js';
-// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
+// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
-class LdSignature {
+class JsonLd {
public debug = false;
public preLoad = true;
public loderTimeout = 5000;
@@ -89,10 +89,18 @@ class LdSignature {
}
@bindThis
- public async normalize(data: JsonLdDocument): Promise<string> {
+ public async compact(data: any, context: any = CONTEXT): Promise<JsonLdDocument> {
const customLoader = this.getLoader();
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
+ return (await import('jsonld')).default.compact(data, context, {
+ documentLoader: customLoader,
+ });
+ }
+
+ @bindThis
+ public async normalize(data: JsonLdDocument): Promise<string> {
+ const customLoader = this.getLoader();
return (await import('jsonld')).default.normalize(data, {
documentLoader: customLoader,
});
@@ -104,11 +112,11 @@ class LdSignature {
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
if (this.preLoad) {
- if (url in CONTEXTS) {
+ if (url in PRELOADED_CONTEXTS) {
if (this.debug) console.debug(`HIT: ${url}`);
return {
contextUrl: undefined,
- document: CONTEXTS[url],
+ document: PRELOADED_CONTEXTS[url],
documentUrl: url,
};
}
@@ -125,7 +133,7 @@ class LdSignature {
}
@bindThis
- private async fetchDocument(url: string): Promise<JsonLd> {
+ private async fetchDocument(url: string): Promise<JsonLdObject> {
const json = await this.httpRequestService.send(
url,
{
@@ -146,7 +154,7 @@ class LdSignature {
}
});
- return json as JsonLd;
+ return json as JsonLdObject;
}
@bindThis
@@ -158,14 +166,14 @@ class LdSignature {
}
@Injectable()
-export class LdSignatureService {
+export class JsonLdService {
constructor(
private httpRequestService: HttpRequestService,
) {
}
@bindThis
- public use(): LdSignature {
- return new LdSignature(this.httpRequestService);
+ public use(): JsonLd {
+ return new JsonLd(this.httpRequestService);
}
}
diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts
index 88afdefcd3..feb8c42c56 100644
--- a/packages/backend/src/core/activitypub/misc/contexts.ts
+++ b/packages/backend/src/core/activitypub/misc/contexts.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import type { JsonLd } from 'jsonld/jsonld-spec.js';
+import type { Context, JsonLd } from 'jsonld/jsonld-spec.js';
/* eslint:disable:quotemark indent */
const id_v1 = {
@@ -526,7 +526,42 @@ const activitystreams = {
},
} satisfies JsonLd;
-export const CONTEXTS: Record<string, JsonLd> = {
+const context_iris = [
+ 'https://www.w3.org/ns/activitystreams',
+ 'https://w3id.org/security/v1',
+];
+
+const extension_context_definition = {
+ Key: 'sec:Key',
+ // as non-standards
+ manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
+ sensitive: 'as:sensitive',
+ Hashtag: 'as:Hashtag',
+ quoteUrl: 'as:quoteUrl',
+ // Mastodon
+ toot: 'http://joinmastodon.org/ns#',
+ Emoji: 'toot:Emoji',
+ featured: 'toot:featured',
+ discoverable: 'toot:discoverable',
+ // schema
+ schema: 'http://schema.org#',
+ PropertyValue: 'schema:PropertyValue',
+ value: 'schema:value',
+ // Misskey
+ misskey: 'https://misskey-hub.net/ns#',
+ '_misskey_content': 'misskey:_misskey_content',
+ '_misskey_quote': 'misskey:_misskey_quote',
+ '_misskey_reaction': 'misskey:_misskey_reaction',
+ '_misskey_votes': 'misskey:_misskey_votes',
+ '_misskey_summary': 'misskey:_misskey_summary',
+ 'isCat': 'misskey:isCat',
+ // vcard
+ vcard: 'http://www.w3.org/2006/vcard/ns#',
+} satisfies Context;
+
+export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition];
+
+export const PRELOADED_CONTEXTS: Record<string, JsonLd> = {
'https://w3id.org/identity/v1': id_v1,
'https://w3id.org/security/v1': security_v1,
'https://www.w3.org/ns/activitystreams': activitystreams,
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index 3addead058..1d05f4ade1 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -15,13 +15,14 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
import FederationChart from '@/core/chart/charts/federation.js';
import { getApId } from '@/core/activitypub/type.js';
+import type { IActivity } from '@/core/activitypub/type.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiUserPublickey } from '@/models/UserPublickey.js';
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 { LdSignatureService } from '@/core/activitypub/LdSignatureService.js';
+import { 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';
@@ -38,7 +39,7 @@ export class InboxProcessorService {
private apInboxService: ApInboxService,
private federatedInstanceService: FederatedInstanceService,
private fetchInstanceMetadataService: FetchInstanceMetadataService,
- private ldSignatureService: LdSignatureService,
+ private jsonLdService: JsonLdService,
private apPersonService: ApPersonService,
private apDbResolverService: ApDbResolverService,
private instanceChart: InstanceChart,
@@ -52,7 +53,7 @@ export class InboxProcessorService {
@bindThis
public async process(job: Bull.Job<InboxJobData>): Promise<string> {
const signature = job.data.signature; // HTTP-signature
- const activity = job.data.activity;
+ let activity = job.data.activity;
//#region Log
const info = Object.assign({}, activity);
@@ -110,20 +111,21 @@ export class InboxProcessorService {
// また、signatureのsignerは、activity.actorと一致する必要がある
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
- if (activity.signature) {
- if (activity.signature.type !== 'RsaSignature2017') {
- throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`);
+ const ldSignature = activity.signature;
+ if (ldSignature) {
+ if (ldSignature.type !== 'RsaSignature2017') {
+ throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${ldSignature.type}`);
}
- // activity.signature.creator: https://example.oom/users/user#main-key
+ // ldSignature.creator: https://example.oom/users/user#main-key
// みたいになっててUserを引っ張れば公開キーも入ることを期待する
- if (activity.signature.creator) {
- const candicate = activity.signature.creator.replace(/#.*/, '');
+ if (ldSignature.creator) {
+ const candicate = ldSignature.creator.replace(/#.*/, '');
await this.apPersonService.resolvePerson(candicate).catch(() => null);
}
// keyIdからLD-Signatureのユーザーを取得
- authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator);
+ authUser = await this.apDbResolverService.getAuthUserFromKeyId(ldSignature.creator);
if (authUser == null) {
throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした');
}
@@ -132,13 +134,31 @@ export class InboxProcessorService {
throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした');
}
+ const jsonLd = this.jsonLdService.use();
+
// LD-Signature検証
- const ldSignature = this.ldSignatureService.use();
- const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
+ 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}`);
+ }
+ // 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);
+ delete compactedInfo['@context'];
+ this.logger.debug(`compacted: ${JSON.stringify(compactedInfo, null, 2)}`);
+ //#endregion
+
// もう一度actorチェック
if (authUser.user.uri !== activity.actor) {
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
--
2.39.1
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment