CVE-2024-39699 (TypeScript, published 2024-07-08T16:15Z)
Description: value: Directus is a real-time API and App dashboard for managing SQL database content. There was already a reported SSRF vulnerability via file import. It was fixed by resolving all DNS names and checking if the requested IP is an internal IP address. However it is possible to bypass this security measure and execute a SSRF using redirects. Directus allows redirects when importing file from the URL and does not check the result URL. Thus, it is possible to execute a request to an internal IP, for example to 127.0.0.1. However, it is blind SSRF, because Directus also uses response interception technique to get the information about the connect from the socket directly and it does not show a response if the IP address is internal. This vulnerability is fixed in 10.9.3.
Severity: nan (CVSS v3 base score: 5)
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
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.
Repository: https://github.com/directus/directus
Files changed:
.changeset/dull-comics-camp.md
api/src/services/roles.test.ts
api/src/services/roles.ts
Commit message: Add validation for IP Access in Role settings (#21444)



Vulnerable code:
import { createTracker, MockClient, Tracker, type RawQuery } from 'knex-mock-client';
	type MockedFunction,
import { ForbiddenError, UnprocessableContentError } from '@directus/errors';
import type { AbstractServiceOptions, Alterations, MutationOptions, PrimaryKey } from '../types/index.js';
	override async updateOne(key: PrimaryKey, data: Record<string, any>, opts?: MutationOptions): Promise<PrimaryKey> {
	override async updateBatch(data: Record<string, any>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
		const primaryKeyField = this.schema.collections[this.collection]!.primary;
	override async updateMany(
		keys: PrimaryKey[],
		data: Record<string, any>,
		opts?: MutationOptions,
	): Promise<PrimaryKey[]> {

Fixed code:
---
'@directus/api': patch
---

Added validation for IP Access in role settings
import { MockClient, Tracker, createTracker, type RawQuery } from 'knex-mock-client';
	type MockedFunction,

			it('should throw due to invalid ip_access', async () => {
				await expect(service.createOne({ ip_access: ['invalid_ip'] })).rejects.toThrow(
					'IP Access contains an incorrect value. Valid values are: IP addresses, IP ranges and CIDR blocks',
				);
			});

			it('should throw due to invalid ip_access', async () => {
				await expect(service.createMany([{ ip_access: ['invalid_ip'] }])).rejects.toThrow(
					'IP Access contains an incorrect value. Valid values are: IP addresses, IP ranges and CIDR blocks',
				);
			});

			it('should throw due to invalid ip_access', async () => {
				await expect(service.updateOne(1, { ip_access: ['invalid_ip'] })).rejects.toThrow(
					'IP Access contains an incorrect value. Valid values are: IP addresses, IP ranges and CIDR blocks',
				);
			});

			it('should throw due to invalid ip_access', async () => {
				await expect(service.updateMany([1], { ip_access: ['invalid_ip'] })).rejects.toThrow(
					'IP Access contains an incorrect value. Valid values are: IP addresses, IP ranges and CIDR blocks',
				);
			});

			it('should throw due to invalid ip_access', async () => {
				await expect(service.updateBatch([{ id: 1, ip_access: ['invalid_ip'] }])).rejects.toThrow(
					'IP Access contains an incorrect value. Valid values are: IP addresses, IP ranges and CIDR blocks',
				);
			});

			it('should throw due to invalid ip_access', async () => {
				await expect(service.updateByQuery({}, { ip_access: ['invalid_ip'] })).rejects.toThrow(
					'IP Access contains an incorrect value. Valid values are: IP addresses, IP ranges and CIDR blocks',
				);
			});
import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
import { getMatch } from 'ip-matching';
import type { AbstractServiceOptions, Alterations, Item, MutationOptions, PrimaryKey } from '../types/index.js';
	private isIpAccessValid(value?: any[] | null): boolean {
		if (value === undefined) return false;
		if (value === null) return true;
		if (Array.isArray(value) && value.length === 0) return true;

		for (const ip of value) {
			if (typeof ip !== 'string' || ip.includes('*')) return false;

			try {
				const match = getMatch(ip);
				if (match.type == 'IPMask') return false;
			} catch {
				return false;
			}
		}

		return true;
	}

	private assertValidIpAccess(partialItem: Partial<Item>): void {
		if ('ip_access' in partialItem && !this.isIpAccessValid(partialItem['ip_access'])) {
			throw new InvalidPayloadError({
				reason: 'IP Access contains an incorrect value. Valid values are: IP addresses, IP ranges and CIDR blocks',
			});
		}
	}

	override async createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
		this.assertValidIpAccess(data);

		return super.createOne(data, opts);
	}

	override async createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
		for (const partialItem of data) {
			this.assertValidIpAccess(partialItem);
		}

		return super.createMany(data, opts);
	}

	override async updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
		this.assertValidIpAccess(data);

	override async updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
		for (const partialItem of data) {
			this.assertValidIpAccess(partialItem);
		}
		const primaryKeyField = this.schema.collections[this.collection]!.primary;
	override async updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]> {
		this.assertValidIpAccess(data);

	override async updateByQuery(
		query: Query,
		data: Partial<Item>,
		opts?: MutationOptions | undefined,
	): Promise<PrimaryKey[]> {
		this.assertValidIpAccess(data);

		return super.updateByQuery(query, data, opts);
	}