CVE-2024-32983 (TypeScript, published 2024-06-03T16:15Z)
Description: value: Misskey is an open source, decentralized microblogging platform. 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. This vulnerability is fixed in 2024.5.0.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/misskey-dev/misskey
Files changed:
CHANGELOG.md
packages/backend/src/core/CoreModule.ts
packages/backend/src/core/activitypub/ApRendererService.ts
packages/backend/src/core/activitypub/JsonLdService.ts
packages/backend/src/core/activitypub/misc/contexts.ts
packages/backend/src/queue/processors/InboxProcessorService.ts
packages/backend/test/unit/activitypub.ts
Commit message: Merge pull request from GHSA-2vxv-pv3m-3wvj

* fix: normalize incoming signed activities

* Tweak style

* Update CHANGELOG.md

* Log compacted activity as well

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

Vulnerable code:
import { LdSignatureService } from './activitypub/LdSignatureService.js';
const $LdSignatureService: Provider = { provide: 'LdSignatureService', useExisting: LdSignatureService };
		LdSignatureService,
		$LdSignatureService,
		LdSignatureService,
		$LdSignatureService,
import { LdSignatureService } from './LdSignatureService.js';
		private ldSignatureService: LdSignatureService,
		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 });
		const ldSignature = this.ldSignatureService.use();
		ldSignature.debug = false;
		activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
import { CONTEXTS } from './misc/contexts.js';
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
class LdSignature {
	public async normalize(data: JsonLdDocument): Promise<string> {
				if (url in CONTEXTS) {
						document: CONTEXTS[url],
	private async fetchDocument(url: string): Promise<JsonLd> {
		return json as JsonLd;
export class LdSignatureService {
	public use(): LdSignature {
		return new LdSignature(this.httpRequestService);
import type { JsonLd } from 'jsonld/jsonld-spec.js';
export const CONTEXTS: Record<string, JsonLd> = {
import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js';
		private ldSignatureService: LdSignatureService,
		const activity = job.data.activity;
			if (activity.signature) {
				if (activity.signature.type !== 'RsaSignature2017') {
					throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`);
				// activity.signature.creator: https://example.oom/users/user#main-key
				if (activity.signature.creator) {
					const candicate = activity.signature.creator.replace(/#.*/, '');
				authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator);
				const ldSignature = this.ldSignatureService.use();
				const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);

Fixed code:
- 悪意のある第三者がリモートユーザーになりすましたアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-2vxv-pv3m-3wvj)をご覧ください。
- Fix: リモートから配送されたアクティビティにJSON-LD compactionをかける
import { JsonLdService } from './activitypub/JsonLdService.js';
const $JsonLdService: Provider = { provide: 'JsonLdService', useExisting: JsonLdService };
		JsonLdService,
		$JsonLdService,
		JsonLdService,
		$JsonLdService,
import { JsonLdService } from './JsonLdService.js';
import { CONTEXT } from './misc/contexts.js';
		private jsonLdService: JsonLdService,
		return Object.assign({ '@context': CONTEXT }, x as T & { id: string });
		const jsonLd = this.jsonLdService.use();
		jsonLd.debug = false;
		activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js';
// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
class JsonLd {
	public async compact(data: any, context: any = CONTEXT): Promise<JsonLdDocument> {
		return (await import('jsonld')).default.compact(data, context, {
			documentLoader: customLoader,
		});
	}

	@bindThis
	public async normalize(data: JsonLdDocument): Promise<string> {
		const customLoader = this.getLoader();
				if (url in PRELOADED_CONTEXTS) {
						document: PRELOADED_CONTEXTS[url],
	private async fetchDocument(url: string): Promise<JsonLdObject> {
		return json as JsonLdObject;
export class JsonLdService {
	public use(): JsonLd {
		return new JsonLd(this.httpRequestService);
import type { Context, JsonLd } from 'jsonld/jsonld-spec.js';
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> = {
import type { IActivity } from '@/core/activitypub/type.js';
import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
		private jsonLdService: JsonLdService,
		let activity = job.data.activity;
			const ldSignature = activity.signature;
			if (ldSignature) {
				if (ldSignature.type !== 'RsaSignature2017') {
					throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${ldSignature.type}`);
				// ldSignature.creator: https://example.oom/users/user#main-key
				if (ldSignature.creator) {
					const candicate = ldSignature.creator.replace(/#.*/, '');
				authUser = await this.apDbResolverService.getAuthUserFromKeyId(ldSignature.creator);
				const jsonLd = this.jsonLdService.use();

				const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
				// アクティビティを正規化
				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

import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
import { CONTEXT } from '@/core/activitypub/misc/contexts.js';
	let jsonLdService: JsonLdService;
		jsonLdService = app.get<JsonLdService>(JsonLdService);

	describe('JSON-LD', () =>{
		test('Compaction', async () => {
			const jsonLd = jsonLdService.use();

			const object = {
				'@context': [
					'https://www.w3.org/ns/activitystreams',
					{
						_misskey_quote: 'https://misskey-hub.net/ns#_misskey_quote',
						unknown: 'https://example.org/ns#unknown',
						undefined: null,
					},
				],
				id: 'https://example.com/notes/42',
				type: 'Note',
				attributedTo: 'https://example.com/users/1',
				to: ['https://www.w3.org/ns/activitystreams#Public'],
				content: 'test test foo',
				_misskey_quote: 'https://example.com/notes/1',
				unknown: 'test test bar',
				undefined: 'test test baz',
			};
			const compacted = await jsonLd.compact(object);

			assert.deepStrictEqual(compacted, {
				'@context': CONTEXT,
				id: 'https://example.com/notes/42',
				type: 'Note',
				attributedTo: 'https://example.com/users/1',
				to: 'as:Public',
				content: 'test test foo',
				_misskey_quote: 'https://example.com/notes/1',
				'https://example.org/ns#unknown': 'test test bar',
				// undefined: 'test test baz',
			});
		});
	});