feat: security fix
This commit is contained in:
parent
e51cae1614
commit
071143dcb0
5 changed files with 68 additions and 7 deletions
|
|
@ -27,6 +27,7 @@ import { Readable, pipeline } from 'stream';
|
|||
import { promisify } from 'util';
|
||||
import { OnlyURL } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
|
||||
import { isSafePublicHttpsUrl } from '@gitroom/nestjs-libraries/dtos/webhooks/webhook.url.validator';
|
||||
import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher';
|
||||
|
||||
const pump = promisify(pipeline);
|
||||
|
||||
|
|
@ -191,6 +192,8 @@ export class PublicController {
|
|||
r = await fetch(currentUrl, {
|
||||
signal: ac.signal,
|
||||
redirect: 'manual',
|
||||
// @ts-ignore — undici option, not in lib.dom fetch types
|
||||
dispatcher: ssrfSafeDispatcher,
|
||||
});
|
||||
|
||||
if (r.status >= 300 && r.status < 400) {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ import { VideoFunctionDto } from '@gitroom/nestjs-libraries/dtos/videos/video.fu
|
|||
import { UploadDto } from '@gitroom/nestjs-libraries/dtos/media/upload.dto';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { GetNotificationsDto } from '@gitroom/nestjs-libraries/dtos/notifications/get.notifications.dto';
|
||||
import axios from 'axios';
|
||||
import { Readable } from 'stream';
|
||||
import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { fromBuffer } = require('file-type');
|
||||
|
||||
|
|
@ -96,11 +96,14 @@ export class PublicIntegrationsController {
|
|||
@Body() body: UploadDto
|
||||
) {
|
||||
Sentry.metrics.count('public_api-request', 1);
|
||||
const response = await axios.get(body.url, {
|
||||
responseType: 'arraybuffer',
|
||||
const response = await fetch(body.url, {
|
||||
// @ts-ignore — undici option, not in lib.dom fetch types
|
||||
dispatcher: ssrfSafeDispatcher,
|
||||
});
|
||||
|
||||
const buffer = Buffer.from(response.data);
|
||||
if (!response.ok) {
|
||||
throw new HttpException({ msg: 'Failed to fetch URL' }, 400);
|
||||
}
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
const detected = await fromBuffer(buffer);
|
||||
if (!detected || !PUBLIC_API_ALLOWED_MIME.has(detected.mime)) {
|
||||
throw new HttpException({ msg: 'Unsupported file type.' }, 400);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { Agent } from 'undici';
|
||||
import dns from 'node:dns';
|
||||
import net from 'node:net';
|
||||
import { isBlockedIp } from './webhook.url.validator';
|
||||
|
||||
// Pins DNS resolution: every resolved IP is checked with `isBlockedIp` and
|
||||
// the caller (undici) connects to that same set. Closes the TOCTOU window
|
||||
// `isSafePublicHttpsUrl` alone leaves open (see GHSA-f7jj-p389-4w45).
|
||||
export const ssrfSafeDispatcher = new Agent({
|
||||
connect: {
|
||||
lookup(hostname, options, callback) {
|
||||
if (net.isIP(hostname)) {
|
||||
const family = net.isIP(hostname);
|
||||
if (isBlockedIp(hostname)) {
|
||||
return callback(new Error('Blocked IP'), '', 0);
|
||||
}
|
||||
return options && (options as any).all
|
||||
? callback(null, [{ address: hostname, family }] as any, family)
|
||||
: callback(null, hostname, family);
|
||||
}
|
||||
|
||||
dns.lookup(hostname, options, (err, address: any, family: any) => {
|
||||
if (err) return callback(err, '', 0);
|
||||
if (Array.isArray(address)) {
|
||||
for (const entry of address) {
|
||||
if (isBlockedIp(entry.address)) {
|
||||
return callback(new Error('Blocked IP'), '', 0);
|
||||
}
|
||||
}
|
||||
return callback(null, address as any, 0);
|
||||
}
|
||||
if (isBlockedIp(address)) {
|
||||
return callback(new Error('Blocked IP'), '', 0);
|
||||
}
|
||||
callback(null, address, family);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -6,6 +6,8 @@ import mime from 'mime-types';
|
|||
import { getExtension } from 'mime';
|
||||
import { IUploadProvider } from './upload.interface';
|
||||
import axios from 'axios';
|
||||
import { isSafePublicHttpsUrl } from '@gitroom/nestjs-libraries/dtos/webhooks/webhook.url.validator';
|
||||
import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { fromBuffer } = require('file-type');
|
||||
|
||||
|
|
@ -75,7 +77,13 @@ class CloudflareStorage implements IUploadProvider {
|
|||
}
|
||||
|
||||
async uploadSimple(path: string) {
|
||||
const loadImage = await fetch(path);
|
||||
if (!(await isSafePublicHttpsUrl(path))) {
|
||||
throw new Error('Unsafe URL');
|
||||
}
|
||||
const loadImage = await fetch(path, {
|
||||
// @ts-ignore — undici option, not in lib.dom fetch types
|
||||
dispatcher: ssrfSafeDispatcher,
|
||||
});
|
||||
const body = Buffer.from(await loadImage.arrayBuffer());
|
||||
const detected = await fromBuffer(body);
|
||||
if (!detected || !ALLOWED_MIME_TYPES.has(detected.mime)) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { mkdirSync, unlink, writeFileSync } from 'fs';
|
|||
// @ts-ignore
|
||||
import mime from 'mime';
|
||||
import { extname } from 'path';
|
||||
import { isSafePublicHttpsUrl } from '@gitroom/nestjs-libraries/dtos/webhooks/webhook.url.validator';
|
||||
import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { fromBuffer } = require('file-type');
|
||||
|
||||
|
|
@ -24,7 +26,13 @@ export class LocalStorage implements IUploadProvider {
|
|||
constructor(private uploadDirectory: string) {}
|
||||
|
||||
async uploadSimple(path: string) {
|
||||
const loadImage = await fetch(path);
|
||||
if (!(await isSafePublicHttpsUrl(path))) {
|
||||
throw new Error('Unsafe URL');
|
||||
}
|
||||
const loadImage = await fetch(path, {
|
||||
// @ts-ignore — undici option, not in lib.dom fetch types
|
||||
dispatcher: ssrfSafeDispatcher,
|
||||
});
|
||||
const contentType =
|
||||
loadImage?.headers?.get('content-type') ||
|
||||
loadImage?.headers?.get('Content-Type');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue