hotfix(backend): GHSA-qqrm-9grj-6v32 (MisskeyIO#460)

* hotfix(backend): GHSA-qqrm-9grj-6v32

Cherry-picked from 9a70ce8f5e

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* security fix: regexp

Co-authored-by: Ry0taK <49341894+Ry0taK@users.noreply.github.com>

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: Ry0taK <49341894+Ry0taK@users.noreply.github.com>
This commit is contained in:
まっちゃとーにゅ 2024-02-17 15:23:56 +09:00 committed by GitHub
parent 8325431d4e
commit d5f229e72b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 144 additions and 16 deletions

View file

@ -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 {
/**
@ -104,6 +111,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, {
@ -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();
@ -169,6 +196,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;
}

View file

@ -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();

View file

@ -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']) ?

View file

@ -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}`);

View file

@ -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 = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/;
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');
}