summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2024-02-17 12:41:19 +0900
committerGitHub <noreply@github.com>2024-02-17 12:41:19 +0900
commit9a70ce8f5ea9df00001894809f5ce7bc69b14c8a (patch)
treef615d85b9fa8298d5009abc3f1dec7561ac82ec1
parent2024.2.0-beta.13 (diff)
downloadsharkey-9a70ce8f5ea9df00001894809f5ce7bc69b14c8a.tar.gz
sharkey-9a70ce8f5ea9df00001894809f5ce7bc69b14c8a.tar.bz2
sharkey-9a70ce8f5ea9df00001894809f5ce7bc69b14c8a.zip
Merge pull request from GHSA-qqrm-9grj-6v32
* maybe ok * fix * test wip * :v: * fix * if (res.ok) * validateContentTypeSetAsJsonLD * 条件を考慮し直す * その他の+json接尾辞が付いているメディアタイプも受け容れる * https://github.com/misskey-dev/misskey-ghsa-qqrm-9grj-6v32/pull/1#discussion_r1490999009 * add `; profile="https://www.w3.org/ns/activitystreams"` * application/ld+json;
-rw-r--r--packages/backend/src/core/HttpRequestService.ts55
-rw-r--r--packages/backend/src/core/activitypub/ApRequestService.ts6
-rw-r--r--packages/backend/src/core/activitypub/ApResolverService.ts2
-rw-r--r--packages/backend/src/core/activitypub/LdSignatureService.ts6
-rw-r--r--packages/backend/src/core/activitypub/misc/validator.ts39
-rw-r--r--packages/backend/test/e2e/fetch-validate-ap-deny.ts40
-rw-r--r--packages/backend/test/unit/activitypub.ts2
-rw-r--r--packages/backend/test/utils.ts36
8 files changed, 157 insertions, 29 deletions
diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts
index b36b9f6e3c..7f3cac7c58 100644
--- a/packages/backend/src/core/HttpRequestService.ts
+++ b/packages/backend/src/core/HttpRequestService.ts
@@ -14,9 +14,16 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js';
+import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
+import type { IObject } from '@/core/activitypub/type.js';
import type { Response } from 'node-fetch';
import type { URL } from 'node:url';
+export type HttpRequestSendOptions = {
+ throwErrorWhenResponseNotOk: boolean;
+ validators?: ((res: Response) => void)[];
+};
+
@Injectable()
export class HttpRequestService {
/**
@@ -105,6 +112,23 @@ export class HttpRequestService {
}
@bindThis
+ public async getActivityJson(url: string): Promise<IObject> {
+ const res = await this.send(url, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+ },
+ timeout: 5000,
+ size: 1024 * 256,
+ }, {
+ throwErrorWhenResponseNotOk: true,
+ validators: [validateContentTypeSetAsActivityPub],
+ });
+
+ return await res.json() as IObject;
+ }
+
+ @bindThis
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
const res = await this.send(url, {
method: 'GET',
@@ -132,17 +156,20 @@ export class HttpRequestService {
}
@bindThis
- public async send(url: string, args: {
- method?: string,
- body?: string,
- headers?: Record<string, string>,
- timeout?: number,
- size?: number,
- } = {}, extra: {
- throwErrorWhenResponseNotOk: boolean;
- } = {
- throwErrorWhenResponseNotOk: true,
- }): Promise<Response> {
+ public async send(
+ url: string,
+ args: {
+ method?: string,
+ body?: string,
+ headers?: Record<string, string>,
+ timeout?: number,
+ size?: number,
+ } = {},
+ extra: HttpRequestSendOptions = {
+ throwErrorWhenResponseNotOk: true,
+ validators: [],
+ },
+ ): Promise<Response> {
const timeout = args.timeout ?? 5000;
const controller = new AbortController();
@@ -166,6 +193,12 @@ export class HttpRequestService {
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
}
+ if (res.ok) {
+ for (const validator of (extra.validators ?? [])) {
+ validator(res);
+ }
+ }
+
return res;
}
}
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index 202e07814e..93ac8ce9a7 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -14,6 +14,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js';
+import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
type Request = {
url: string;
@@ -70,7 +71,7 @@ export class ApRequestCreator {
url: u.href,
method: 'GET',
headers: this.#objectAssignWithLcKey({
- 'Accept': 'application/activity+json, application/ld+json',
+ 'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'Date': new Date().toUTCString(),
'Host': new URL(args.url).host,
}, args.additionalHeaders),
@@ -195,6 +196,9 @@ export class ApRequestService {
const res = await this.httpRequestService.send(url, {
method: req.request.method,
headers: req.request.headers,
+ }, {
+ throwErrorWhenResponseNotOk: true,
+ validators: [validateContentTypeSetAsActivityPub],
});
return await res.json();
diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts
index db44c042e7..bb3c40f093 100644
--- a/packages/backend/src/core/activitypub/ApResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApResolverService.ts
@@ -105,7 +105,7 @@ export class Resolver {
const object = (this.user
? await this.apRequestService.signedGet(value, this.user) as IObject
- : await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
+ : await this.httpRequestService.getActivityJson(value)) as IObject;
if (
Array.isArray(object['@context']) ?
diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts
index f958e9d16e..9de184336f 100644
--- a/packages/backend/src/core/activitypub/LdSignatureService.ts
+++ b/packages/backend/src/core/activitypub/LdSignatureService.ts
@@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import { 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';
@@ -133,7 +134,10 @@ class LdSignature {
},
timeout: this.loderTimeout,
},
- { throwErrorWhenResponseNotOk: false },
+ {
+ throwErrorWhenResponseNotOk: false,
+ validators: [validateContentTypeSetAsJsonLD],
+ },
).then(res => {
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`);
diff --git a/packages/backend/src/core/activitypub/misc/validator.ts b/packages/backend/src/core/activitypub/misc/validator.ts
new file mode 100644
index 0000000000..6ba14a222f
--- /dev/null
+++ b/packages/backend/src/core/activitypub/misc/validator.ts
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Response } from 'node-fetch';
+
+export function validateContentTypeSetAsActivityPub(response: Response): void {
+ const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
+
+ if (contentType === '') {
+ throw new Error('Validate content type of AP response: No content-type header');
+ }
+ if (
+ contentType.startsWith('application/activity+json') ||
+ (contentType.startsWith('application/ld+json;') && contentType.includes('https://www.w3.org/ns/activitystreams'))
+ ) {
+ return;
+ }
+ throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json');
+}
+
+const plusJsonSuffixRegex = /(application|text)\/[a-zA-Z0-9\.\-\+]+\+json/;
+
+export function validateContentTypeSetAsJsonLD(response: Response): void {
+ const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
+
+ if (contentType === '') {
+ throw new Error('Validate content type of JSON LD: No content-type header');
+ }
+ if (
+ contentType.startsWith('application/ld+json') ||
+ contentType.startsWith('application/json') ||
+ plusJsonSuffixRegex.test(contentType)
+ ) {
+ return;
+ }
+ throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json');
+}
diff --git a/packages/backend/test/e2e/fetch-validate-ap-deny.ts b/packages/backend/test/e2e/fetch-validate-ap-deny.ts
new file mode 100644
index 0000000000..434a9fe209
--- /dev/null
+++ b/packages/backend/test/e2e/fetch-validate-ap-deny.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+process.env.NODE_ENV = 'test';
+
+import { validateContentTypeSetAsActivityPub, validateContentTypeSetAsJsonLD } from '@/core/activitypub/misc/validator.js';
+import { signup, uploadFile, relativeFetch } from '../utils.js';
+import type * as misskey from 'misskey-js';
+
+describe('validateContentTypeSetAsActivityPub/JsonLD (deny case)', () => {
+ let alice: misskey.entities.SignupResponse;
+ let aliceUploadedFile: any;
+
+ beforeAll(async () => {
+ alice = await signup({ username: 'alice' });
+ aliceUploadedFile = await uploadFile(alice);
+ }, 1000 * 60 * 2);
+
+ test('ActivityStreams: ファイルはエラーになる', async () => {
+ const res = await relativeFetch(aliceUploadedFile.webpublicUrl);
+
+ function doValidate() {
+ validateContentTypeSetAsActivityPub(res);
+ }
+
+ expect(doValidate).toThrow('Content type is not');
+ });
+
+ test('JSON-LD: ファイルはエラーになる', async () => {
+ const res = await relativeFetch(aliceUploadedFile.webpublicUrl);
+
+ function doValidate() {
+ validateContentTypeSetAsJsonLD(res);
+ }
+
+ expect(doValidate).toThrow('Content type is not');
+ });
+});
diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts
index 88ff49b119..b4b06b06bd 100644
--- a/packages/backend/test/unit/activitypub.ts
+++ b/packages/backend/test/unit/activitypub.ts
@@ -203,7 +203,7 @@ describe('ActivityPub', () => {
describe('Renderer', () => {
test('Render an announce with visibility: followers', () => {
- rendererService.renderAnnounce(null, {
+ rendererService.renderAnnounce('https://example.com/notes/00example', {
id: genAidx(Date.now()),
visibility: 'followers',
} as MiNote);
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index d5da8e0226..a2220ffae6 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -13,10 +13,11 @@ import fetch, { File, RequestInit } from 'node-fetch';
import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
+import { Packed } from '@/misc/json-schema.js';
+import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js';
import type * as misskey from 'misskey-js';
-import { Packed } from '@/misc/json-schema.js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
@@ -123,9 +124,9 @@ export function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', len
function timeoutPromise<T>(p: Promise<T>, timeout: number): Promise<T> {
return Promise.race([
p,
- new Promise((reject) =>{
- setTimeout(() => { reject(new Error('timed out')); }, timeout)
- }) as never
+ new Promise((reject) => {
+ setTimeout(() => { reject(new Error('timed out')); }, timeout);
+ }) as never,
]);
}
@@ -327,7 +328,6 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO
});
const body = res.status !== 204 ? await res.json() as misskey.Endpoints['drive/files/create']['res'] : null;
-
return {
status: res.status,
headers: res.headers,
@@ -343,7 +343,7 @@ export const uploadUrl = async (user: UserToken, url: string): Promise<Packed<'D
'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>,
- 60 * 1000
+ 60 * 1000,
);
await api('drive/files/upload-from-url', {
@@ -434,20 +434,20 @@ export const waitFire = async (user: UserToken, channel: string, trgr: () => any
* @returns 時間内に正常に処理できた場合に通知からextractorを通した値を得る
*/
export function makeStreamCatcher<T>(
- user: UserToken,
- channel: string,
- cond: (message: Record<string, any>) => boolean,
- extractor: (message: Record<string, any>) => T,
- timeout = 60 * 1000): Promise<T> {
- let ws: WebSocket
+ user: UserToken,
+ channel: string,
+ cond: (message: Record<string, any>) => boolean,
+ extractor: (message: Record<string, any>) => T,
+ timeout = 60 * 1000): Promise<T> {
+ let ws: WebSocket;
const p = new Promise<T>(async (resolve) => {
ws = await connectStream(user, channel, (msg) => {
if (cond(msg)) {
- resolve(extractor(msg))
+ resolve(extractor(msg));
}
});
}).finally(() => {
- ws?.close();
+ ws.close();
});
return timeoutPromise(p, timeout);
@@ -476,6 +476,14 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
'text/html; charset=utf-8',
];
+ if (res.ok && (
+ accept.startsWith('application/activity+json') ||
+ (accept.startsWith('application/ld+json') && accept.includes('https://www.w3.org/ns/activitystreams'))
+ )) {
+ // validateContentTypeSetAsActivityPubのテストを兼ねる
+ validateContentTypeSetAsActivityPub(res);
+ }
+
const body =
jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() :
htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) :