CVE-2022-23080 (published 2022-06-22T16:15Z)

In directus versions v9.0.0-beta.2 through 9.6.0 are vulnerable to server-side request forgery (SSRF) in the media upload functionality which allows a low privileged user to perform internal network port scans.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:P/I:N/A:N)
CVSS v3 base score: 5.0, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/directus/directus
commit_details: repo_url: https://github.com/directus/directus
author: Rijk van Zanten
author_date: 2022-03-07 18:09:08-05:00
msg: Add support for import ip deny list (#12025)

* Add support for import ip deny list

* Fix typo
repository: repo_url: https://github.com/directus/directus
repo_name: directus/directus
description: The Modern Data Stack 🐰 — Directus is an instant REST+GraphQL API and intuitive no-code data collaboration app for any SQL database.
repo_language: TypeScript
owner: directus
file_changes: filename: env.ts
change_type: ModificationType.MODIFY
diff: @@ -77,6 +77,8 @@ const defaults: Record<string, any> = {
 	IP_TRUST_PROXY: true,
 	IP_CUSTOM_HEADER: false,
 
+	IMPORT_IP_DENY_LIST: '0.0.0.0',
+
 	SERVE_APP: true,
 
 	RELATIONAL_BATCH_SIZE: 25000,
@@ -95,6 +97,7 @@ const typeMap: Record<string, string> = {
 	DB_PORT: 'number',
 
 	DB_EXCLUDE_TABLES: 'array',
+	IMPORT_IP_DENY_LIST: 'array',
 };
 
 let env: Record<string, any> = {

programming_language: JavaScript
filename: files.ts
change_type: ModificationType.MODIFY
diff: @@ -5,7 +5,9 @@ import { clone } from 'lodash';
 import { extension } from 'mime-types';
 import path from 'path';
 import sharp from 'sharp';
-import url from 'url';
+import url, { URL } from 'url';
+import { promisify } from 'util';
+import { lookup } from 'dns';
 import emitter from '../emitter';
 import env from '../env';
 import { ForbiddenException, ServiceUnavailableException } from '../exceptions';
@@ -14,6 +16,10 @@ import storage from '../storage';
 import { AbstractServiceOptions, File, PrimaryKey, MutationOptions } from '../types';
 import { toArray } from '@directus/shared/utils';
 import { ItemsService } from './items';
+import net from 'net';
+import os from 'os';
+
+const lookupDNS = promisify(lookup);
 
 export class FilesService extends ItemsService {
 	constructor(options: AbstractServiceOptions) {
@@ -161,6 +167,54 @@ export class FilesService extends ItemsService {
 			throw new ForbiddenException();
 		}
 
+		let resolvedUrl;
+
+		try {
+			resolvedUrl = new URL(importURL);
+		} catch (err: any) {
+			logger.warn(err, `Requested URL ${importURL} isn't a valid URL`);
+			throw new ServiceUnavailableException(`Couldn't fetch file from url "${importURL}"`, {
+				service: 'external-file',
+			});
+		}
+
+		let ip = resolvedUrl.hostname;
+
+		if (net.isIP(ip) === 0) {
+			try {
+				ip = (await lookupDNS(ip)).address;
+			} catch (err: any) {
+				logger.warn(err, `Couldn't lookup the DNS for url ${importURL}`);
+				throw new ServiceUnavailableException(`Couldn't fetch file from url "${importURL}"`, {
+					service: 'external-file',
+				});
+			}
+		}
+
+		if (env.IMPORT_IP_DENY_LIST.includes('0.0.0.0')) {
+			const networkInterfaces = os.networkInterfaces();
+
+			for (const networkInfo of Object.values(networkInterfaces)) {
+				if (!networkInfo) continue;
+
+				for (const info of networkInfo) {
+					if (info.address === ip) {
+						logger.warn(`Requested URL ${importURL} resolves to localhost.`);
+						throw new ServiceUnavailableException(`Couldn't fetch file from url "${importURL}"`, {
+							service: 'external-file',
+						});
+					}
+				}
+			}
+		}
+
+		if (env.IMPORT_IP_DENY_LIST.includes(ip)) {
+			logger.warn(`Requested URL ${importURL} resolves to a denied IP address.`);
+			throw new ServiceUnavailableException(`Couldn't fetch file from url "${importURL}"`, {
+				service: 'external-file',
+			});
+		}
+
 		let fileResponse: AxiosResponse<NodeJS.ReadableStream>;
 
 		try {
@@ -168,8 +222,7 @@ export class FilesService extends ItemsService {
 				responseType: 'stream',
 			});
 		} catch (err: any) {
-			logger.warn(`Couldn't fetch file from url "${importURL}"`);
-			logger.warn(err);
+			logger.warn(err, `Couldn't fetch file from url "${importURL}"`);
 			throw new ServiceUnavailableException(`Couldn't fetch file from url "${importURL}"`, {
 				service: 'external-file',
 			});

programming_language: TypeScript
filename: config-options.md
change_type: ModificationType.MODIFY
diff: @@ -280,6 +280,7 @@ All the `DB_POOL_` prefixed options are passed to [`tarn.js`](https://github.com
 | `IP_CUSTOM_HEADER`               | What custom request header to use for the IP address                                                                                                             | false                    |
 | `CONTENT_SECURITY_POLICY`        | Custom overrides for the Content-Security-Policy header. See [helmet's documentation](https://helmetjs.github.io) for more information.                          | --                       |
 | `ASSETS_CONTENT_SECURITY_POLICY` | Custom overrides for the Content-Security-Policy header for the /assets endpoint. See [helmet's documentation](https://helmetjs.github.io) for more information. | --                       |
+| `IMPORT_IP_DENY_LIST`            | Deny importing files from these IP addresses. Use `0.0.0.0` for any local IP address                                                                             | `0.0.0.0`                |
 
 ::: tip Cookie Strictness
 

programming_language: Markdown