CVE-2022-22117: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: In Directus, versions 9.0.0-alpha.4 through 9.4.1 allow unrestricted file upload of .html files in the media upload functionality, which leads to Cross-Site Scripting vulnerability. A low privileged attacker can upload a crafted HTML file as a profile avatar, and when an admin or another user opens it, the XSS payload gets triggered.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2022-01-10T16:15Z
Repository: https://github.com/directus/directus
Language: JSON
Files changed: api/package.json, api/src/app.ts, docs/configuration/config-options.md, package-lock.json
Commit date: 2021-12-30 16:51:49 -0500
Commit message: Add Content-Security-Policy header by default (#10776)


Diff: diff --git a/api/package.json b/api/package.json
index 2815fa3b0..e4e512ece 100644
--- a/api/package.json
+++ b/api/package.json
@@ -112,10 +112,11 @@
 		"express": "^4.17.1",
 		"flat": "^5.0.2",
 		"fs-extra": "^10.0.0",
 		"graphql": "^15.5.0",
 		"graphql-compose": "^9.0.1",
+		"helmet": "^4.6.0",
 		"inquirer": "^8.1.1",
 		"joi": "^17.3.0",
 		"js-yaml": "^4.1.0",
 		"js2xmlparser": "^4.0.1",
 		"json2csv": "^5.0.3",
diff --git a/api/src/app.ts b/api/src/app.ts
index 210d68d17..02f0b005a 100644
--- a/api/src/app.ts
+++ b/api/src/app.ts
@@ -1,10 +1,11 @@
 import cookieParser from 'cookie-parser';
 import express, { Request, Response, RequestHandler } from 'express';
 import fse from 'fs-extra';
 import path from 'path';
 import qs from 'qs';
+import helmet from 'helmet';
 
 import activityRouter from './controllers/activity';
 import assetsRouter from './controllers/assets';
 import authRouter from './controllers/auth';
 import collectionsRouter from './controllers/collections';
@@ -51,10 +52,11 @@ import { validateEnv } from './utils/validate-env';
 import { validateStorage } from './utils/validate-storage';
 import { register as registerWebhooks } from './webhooks';
 import { flushCaches } from './cache';
 import { registerAuthProviders } from './auth';
 import { Url } from './utils/url';
+import { getConfigFromEnv } from './utils/get-config-from-env';
 
 export default async function createApp(): Promise<express.Application> {
 	validateEnv(['KEY', 'SECRET']);
 
 	if (!new Url(env.PUBLIC_URL).isAbsolute()) {
@@ -87,10 +89,12 @@ export default async function createApp(): Promise<express.Application> {
 
 	app.disable('x-powered-by');
 	app.set('trust proxy', env.IP_TRUST_PROXY);
 	app.set('query parser', (str: string) => qs.parse(str, { depth: 10 }));
 
+	app.use(helmet.contentSecurityPolicy(getConfigFromEnv('CONTENT_SECURITY_POLICY_')));
+
 	await emitter.emitInit('app.before', { app });
 
 	await emitter.emitInit('middlewares.before', { app });
 
 	app.use(expressLogger);
diff --git a/docs/configuration/config-options.md b/docs/configuration/config-options.md
index 1c0251b03..44390b5df 100644
--- a/docs/configuration/config-options.md
+++ b/docs/configuration/config-options.md
@@ -225,24 +225,25 @@ All the `DB_POOL_` prefixed options are passed to [`tarn.js`](https://github.com
 
 :::
 
 ## Security
 
-| Variable                         | Description                                                                                                            | Default Value            |
-| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------ |
-| `KEY`                            | Unique identifier for the project.                                                                                     | --                       |
-| `SECRET`                         | Secret string for the project.                                                                                         | --                       |
-| `ACCESS_TOKEN_TTL`               | The duration that the access token is valid.                                                                           | `15m`                    |
-| `REFRESH_TOKEN_TTL`              | The duration that the refresh token is valid, and also how long users stay logged-in to the App.                       | `7d`                     |
-| `REFRESH_TOKEN_COOKIE_DOMAIN`    | Which domain to use for the refresh cookie. Useful for development mode.                                               | --                       |
-| `REFRESH_TOKEN_COOKIE_SECURE`    | Whether or not to use a secure cookie for the refresh token in cookie mode.                                            | `false`                  |
-| `REFRESH_TOKEN_COOKIE_SAME_SITE` | Value for `sameSite` in the refresh token cookie when in cookie mode.                                                  | `lax`                    |
-| `REFRESH_TOKEN_COOKIE_NAME`      | Name of refresh token cookie .                                                                                         | `directus_refresh_token` |
-| `PASSWORD_RESET_URL_ALLOW_LIST`  | List of URLs that can be used [as `reset_url` in /password/request](/reference/authentication/#request-password-reset) | --                       |
-| `USER_INVITE_URL_ALLOW_LIST`     | List of URLs that can be used [as `invite_url` in /users/invite](/reference/system/users/#invite-a-new-user)           | --                       |
-| `IP_TRUST_PROXY`                 | Settings for [express' trust proxy setting](https://expressjs.com/en/guide/behind-proxies.html)                        | true                     |
-| `IP_CUSTOM_HEADER`               | What custom request header to use for the IP address                                                                   | false                    |
+| Variable                         | Description                                                                                                                           | Default Value            |
+| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
+| `KEY`                            | Unique identifier for the project.                                                                                                    | --                       |
+| `SECRET`                         | Secret string for the project.                                                                                                        | --                       |
+| `ACCESS_TOKEN_TTL`               | The duration that the access token is valid.                                                                                          | `15m`                    |
+| `REFRESH_TOKEN_TTL`              | The duration that the refresh token is valid, and also how long users stay logged-in to the App.                                      | `7d`                     |
+| `REFRESH_TOKEN_COOKIE_DOMAIN`    | Which domain to use for the refresh cookie. Useful for development mode.                                                              | --                       |
+| `REFRESH_TOKEN_COOKIE_SECURE`    | Whether or not to use a secure cookie for the refresh token in cookie mode.                                                           | `false`                  |
+| `REFRESH_TOKEN_COOKIE_SAME_SITE` | Value for `sameSite` in the refresh token cookie when in cookie mode.                                                                 | `lax`                    |
+| `REFRESH_TOKEN_COOKIE_NAME`      | Name of refresh token cookie .                                                                                                        | `directus_refresh_token` |
+| `PASSWORD_RESET_URL_ALLOW_LIST`  | List of URLs that can be used [as `reset_url` in /password/request](/reference/authentication/#request-password-reset)                | --                       |
+| `USER_INVITE_URL_ALLOW_LIST`     | List of URLs that can be used [as `invite_url` in /users/invite](/reference/system/users/#invite-a-new-user)                          | --                       |
+| `IP_TRUST_PROXY`                 | Settings for [express' trust proxy setting](https://expressjs.com/en/guide/behind-proxies.html)                                       | true                     |
+| `IP_CUSTOM_HEADER`               | What custom request header to use for the IP address                                                                                  | false                    |
+| `CONTENT_SECURITY_POLICY`        | Custom options for the Content-Security-Policy header. See [helmet's documentation](https://helmetjs.github.io) for more information. | --                       |
 
 ::: tip Cookie Strictness
 
 Browser are pretty strict when it comes to third-party cookies. If you're running into unexpected problems when running
 your project and API on different domains, make sure to verify your configuration for `REFRESH_TOKEN_COOKIE_NAME`,
diff --git a/package-lock.json b/package-lock.json
index 82f6c7867..935035d8b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -99,10 +99,11 @@
 				"express": "^4.17.1",
 				"flat": "^5.0.2",
 				"fs-extra": "^10.0.0",
 				"graphql": "^15.5.0",
 				"graphql-compose": "^9.0.1",
+				"helmet": "^4.6.0",
 				"inquirer": "^8.1.1",
 				"joi": "^17.3.0",
 				"js-yaml": "^4.1.0",
 				"js2xmlparser": "^4.0.1",
 				"json2csv": "^5.0.3",
@@ -23938,10 +23939,18 @@
 			"license": "MIT",
 			"bin": {
 				"he": "bin/he"
 			}
 		},
+		"node_modules/helmet": {
+			"version": "4.6.0",
+			"resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz",
+			"integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==",
+			"engines": {
+				"node": ">=10.0.0"
+			}
+		},
 		"node_modules/hex-color-regex": {
 			"version": "1.1.0",
 			"license": "MIT"
 		},
 		"node_modules/hicat": {
@@ -58126,10 +58135,11 @@
 				"express": "^4.17.1",
 				"flat": "^5.0.2",
 				"fs-extra": "^10.0.0",
 				"graphql": "^15.5.0",
 				"graphql-compose": "^9.0.1",
+				"helmet": "*",
 				"inquirer": "^8.1.1",
 				"ioredis": "^4.27.6",
 				"jest": "27.3.1",
 				"joi": "^17.3.0",
 				"js-yaml": "^4.1.0",
@@ -62590,10 +62600,15 @@
 		},
 		"he": {
 			"version": "1.2.0",
 			"dev": true
 		},
+		"helmet": {
+			"version": "4.6.0",
+			"resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz",
+			"integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg=="
+		},
 		"hex-color-regex": {
 			"version": "1.1.0"
 		},
 		"hicat": {
 			"version": "0.8.0",

Vulnerable code: | Variable                         | Description                                                                                                            | Default Value            |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| `KEY`                            | Unique identifier for the project.                                                                                     | --                       |
| `SECRET`                         | Secret string for the project.                                                                                         | --                       |
| `ACCESS_TOKEN_TTL`               | The duration that the access token is valid.                                                                           | `15m`                    |
| `REFRESH_TOKEN_TTL`              | The duration that the refresh token is valid, and also how long users stay logged-in to the App.                       | `7d`                     |
| `REFRESH_TOKEN_COOKIE_DOMAIN`    | Which domain to use for the refresh cookie. Useful for development mode.                                               | --                       |
| `REFRESH_TOKEN_COOKIE_SECURE`    | Whether or not to use a secure cookie for the refresh token in cookie mode.                                            | `false`                  |
| `REFRESH_TOKEN_COOKIE_SAME_SITE` | Value for `sameSite` in the refresh token cookie when in cookie mode.                                                  | `lax`                    |
| `REFRESH_TOKEN_COOKIE_NAME`      | Name of refresh token cookie .                                                                                         | `directus_refresh_token` |
| `PASSWORD_RESET_URL_ALLOW_LIST`  | List of URLs that can be used [as `reset_url` in /password/request](/reference/authentication/#request-password-reset) | --                       |
| `USER_INVITE_URL_ALLOW_LIST`     | List of URLs that can be used [as `invite_url` in /users/invite](/reference/system/users/#invite-a-new-user)           | --                       |
| `IP_TRUST_PROXY`                 | Settings for [express' trust proxy setting](https://expressjs.com/en/guide/behind-proxies.html)                        | true                     |
| `IP_CUSTOM_HEADER`               | What custom request header to use for the IP address                                                                   | false                    |
Fixed code: 		"helmet": "^4.6.0",
import helmet from 'helmet';
import { getConfigFromEnv } from './utils/get-config-from-env';
	app.use(helmet.contentSecurityPolicy(getConfigFromEnv('CONTENT_SECURITY_POLICY_')));

| Variable                         | Description                                                                                                                           | Default Value            |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| `KEY`                            | Unique identifier for the project.                                                                                                    | --                       |
| `SECRET`                         | Secret string for the project.                                                                                                        | --                       |
| `ACCESS_TOKEN_TTL`               | The duration that the access token is valid.                                                                                          | `15m`                    |
| `REFRESH_TOKEN_TTL`              | The duration that the refresh token is valid, and also how long users stay logged-in to the App.                                      | `7d`                     |
| `REFRESH_TOKEN_COOKIE_DOMAIN`    | Which domain to use for the refresh cookie. Useful for development mode.                                                              | --                       |
| `REFRESH_TOKEN_COOKIE_SECURE`    | Whether or not to use a secure cookie for the refresh token in cookie mode.                                                           | `false`                  |
| `REFRESH_TOKEN_COOKIE_SAME_SITE` | Value for `sameSite` in the refresh token cookie when in cookie mode.                                                                 | `lax`                    |
| `REFRESH_TOKEN_COOKIE_NAME`      | Name of refresh token cookie .                                                                                                        | `directus_refresh_token` |
| `PASSWORD_RESET_URL_ALLOW_LIST`  | List of URLs that can be used [as `reset_url` in /password/request](/reference/authentication/#request-password-reset)                | --                       |
| `USER_INVITE_URL_ALLOW_LIST`     | List of URLs that can be used [as `invite_url` in /users/invite](/reference/system/users/#invite-a-new-user)                          | --                       |
| `IP_TRUST_PROXY`                 | Settings for [express' trust proxy setting](https://expressjs.com/en/guide/behind-proxies.html)                                       | true                     |
| `IP_CUSTOM_HEADER`               | What custom request header to use for the IP address                                                                                  | false                    |
| `CONTENT_SECURITY_POLICY`        | Custom options for the Content-Security-Policy header. See [helmet's documentation](https://helmetjs.github.io) for more information. | --                       |
				"helmet": "^4.6.0",
		"node_modules/helmet": {
			"version": "4.6.0",
			"resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz",
			"integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==",
			"engines": {
				"node": ">=10.0.0"
			}
		},
				"helmet": "*",
		"helmet": {
			"version": "4.6.0",
			"resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz",
			"integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg=="
		},