CVE-2023-28443 (published 2023-03-24T00:15Z)

Directus is a real-time API and App dashboard for managing SQL database content. Prior to version 9.23.3, the `directus_refresh_token` is not redacted properly from the log outputs and can be used to impersonate users without their permission. This issue is patched in version 9.23.3.

CVSS v3 base score: 5.5, MEDIUM (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-284
cwe_name: Improper Access Control
description: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
extended_description: 
            Access control involves the use of several protection mechanisms such as:
            
               Authentication (proving the identity of an actor)
               Authorization (ensuring that a given actor can access a resource), and
               Accountability (tracking of activities that were performed)
            
            When any mechanism is not applied or otherwise fails, attackers can compromise the security of the product by gaining privileges, reading sensitive information, executing commands, evading detection, etc.
            There are two distinct behaviors that can introduce access control weaknesses:
               
                  Specification: incorrect privileges, permissions, ownership, etc. are explicitly specified for either the user or the resource (for example, setting a password file to be world-writable, or giving administrator capabilities to a guest user). This action could be performed by the program or the administrator.
                  Enforcement: the mechanism contains errors that prevent it from properly enforcing the specified access control requirements (e.g., allowing the user to specify their own privileges, or allowing a syntactically-incorrect ACL to produce insecure settings). This problem occurs within the program itself, in that it does not actually enforce the intended security policy that the administrator specifies.
               
         
		 
cwe_id: CWE-532
cwe_name: Insertion of Sensitive Information into Log File
description: Information written to log files can be of a sensitive nature and give valuable guidance to an attacker or expose sensitive user information.
extended_description: 
            While logging all information may be helpful during development stages, it is important that logging levels be set appropriately before a product ships so that sensitive user data and system information are not accidentally exposed to potential attackers.
            Different log files may be produced and stored for:
            
               Server log files (e.g. server.log).  This can give information on whatever application left the file. Usually this can give full path names and system information, and sometimes usernames and passwords.
               log files that are used for debugging
            
         
         

Fix information:
repo_url: https://github.com/directus/directus
commit_details: repo_url: https://github.com/directus/directus
author: John Hillegass
author_date: 2023-03-23 13:54:02-04:00
msg: Merge pull request from GHSA-8vg2-wf3q-mwv7

* fix(api): redact header cookie

This is a quick PoC for a fix. I am not sure if it's the best answer and have not added tests yet. If we feel good about it, I can add tests and open a PR. Please let me know how you'd like to proceed!

* cleaner

* rework to handle multiple inputs and add unit tests

* Added same redacting logic for teh response set-cookie

---------

Co-authored-by: Brainslug <tim@brainslug.nl>
Co-authored-by: Brainslug <br41nslug@users.noreply.github.com>
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: logger.ts
change_type: ModificationType.MODIFY
diff: @@ -1,11 +1,12 @@
 import { toArray } from '@directus/shared/utils';
-import type { Request, RequestHandler } from 'express';
 import { merge } from 'lodash';
-import pino, { LoggerOptions } from 'pino';
+import pino, { LoggerOptions, SerializedResponse } from 'pino';
+import type { Request, RequestHandler } from 'express';
 import pinoHTTP, { stdSerializers } from 'pino-http';
 import { URL } from 'url';
 import env from './env';
 import { getConfigFromEnv } from './utils/get-config-from-env';
+import { redactHeaderCookie } from './utils/redact-header-cookies';
 
 const pinoOptions: LoggerOptions = {
 	level: env.LOG_LEVEL || 'info',
@@ -86,8 +87,23 @@ export const expressLogger = pinoHTTP({
 		req(request: Request) {
 			const output = stdSerializers.req(request);
 			output.url = redactQuery(output.url);
+			if (output.headers?.cookie) {
+				output.headers.cookie = redactHeaderCookie(output.headers.cookie, [
+					'access_token',
+					`${env.REFRESH_TOKEN_COOKIE_NAME}`,
+				]);
+			}
 			return output;
 		},
+		res(response: SerializedResponse) {
+			if (response.headers?.['set-cookie']) {
+				response.headers['set-cookie'] = redactHeaderCookie(response.headers['set-cookie'], [
+					'access_token',
+					`${env.REFRESH_TOKEN_COOKIE_NAME}`,
+				]);
+			}
+			return response;
+		},
 	},
 }) as RequestHandler;
 

programming_language: TypeScript
filename: redact-header-cookies.test.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,42 @@
+import { describe, expect, test } from 'vitest';
+import env from '../env';
+import { redactHeaderCookie } from './redact-header-cookies';
+
+describe('redactHeaderCookie', () => {
+	describe('Given auth cookies', () => {
+		test('When it finds a refresh_token, it should redact the value', () => {
+			const tokenKey = env.REFRESH_TOKEN_COOKIE_NAME;
+			const cookieHeader = `${tokenKey}=shh;`;
+			const cookieNames = [`${tokenKey}`];
+
+			const redactedCookie = redactHeaderCookie(cookieHeader, cookieNames);
+			expect(redactedCookie).toBe(`${tokenKey}=--redacted--;`);
+		});
+		test('When it finds an access_token, it should redact the value', () => {
+			const tokenKey = 'access_token';
+			const cookieHeader = `${tokenKey}=secret;`;
+			const cookieNames = [`${tokenKey}`];
+
+			const redactedCookie = redactHeaderCookie(cookieHeader, cookieNames);
+			expect(redactedCookie).toBe(`${tokenKey}=--redacted--;`);
+		});
+		test('When it finds both an access_token and refresh_token, it should redact both values', () => {
+			const cookieHeader = `access_token=secret; ${env.REFRESH_TOKEN_COOKIE_NAME}=shhhhhhh; randomCookie=Erdtree;`;
+			const cookieNames = ['access_token', `${env.REFRESH_TOKEN_COOKIE_NAME}`];
+
+			const redactedCookie = redactHeaderCookie(cookieHeader, cookieNames);
+			expect(redactedCookie).toBe(
+				`access_token=--redacted--; ${env.REFRESH_TOKEN_COOKIE_NAME}=--redacted--; randomCookie=Erdtree;`
+			);
+		});
+	});
+	describe('Given negligible cookies', () => {
+		test('It should return the orignal value', () => {
+			const originalCookie = `Crown=Swords; Hail=Sithis;`;
+			const cookieNames = [env.REFRESH_TOKEN_COOKIE_NAME, 'access_token'];
+
+			const redactedCookie = redactHeaderCookie(originalCookie, cookieNames);
+			expect(redactedCookie).toBe(originalCookie);
+		});
+	});
+});

programming_language: TypeScript
filename: redact-header-cookies.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,7 @@
+export function redactHeaderCookie(cookieHeader: string, cookieNames: string[]) {
+	for (const cookieName of cookieNames) {
+		const re = new RegExp(`(${cookieName}=)([^;]+)`);
+		cookieHeader = cookieHeader.replace(re, `$1--redacted--`);
+	}
+	return cookieHeader;
+}

programming_language: TypeScript