@wbce-d9/api 12.5.9 → 12.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -109,6 +109,7 @@ export default async function createApp() {
109
109
  if (env['HSTS_ENABLED']) {
110
110
  app.use(helmet.hsts(getConfigFromEnv('HSTS_', ['HSTS_ENABLED'])));
111
111
  }
112
+ app.use(helmet.crossOriginOpenerPolicy({ policy: env['CROSS_ORIGIN_OPENER_POLICY'] }));
112
113
  await emitter.emitInit('app.before', { app });
113
114
  await emitter.emitInit('middlewares.before', { app });
114
115
  app.use(expressLogger);
@@ -17,6 +17,23 @@ import { getIPFromReq } from '../../utils/get-ip-from-req.js';
17
17
  import { Url } from '../../utils/url.js';
18
18
  import { isRedirectAllowedOnLogin } from '../../utils/is-redirect-allowed-on-login.js';
19
19
  import { BaseOAuthDriver } from './baseoauth.js';
20
+ // Authorization-request parameters the caller must never control
21
+ const RESERVED_AUTH_PARAMS = new Set([
22
+ 'prompt',
23
+ 'redirect',
24
+ 'scope',
25
+ 'access_type',
26
+ 'code_challenge',
27
+ 'code_challenge_method',
28
+ 'state',
29
+ 'nonce',
30
+ 'redirect_uri',
31
+ 'client_id',
32
+ 'response_type',
33
+ 'response_mode',
34
+ 'request',
35
+ 'request_uri',
36
+ ]);
20
37
  export class OpenIDAuthDriver extends BaseOAuthDriver {
21
38
  client;
22
39
  redirectUrl;
@@ -75,6 +92,7 @@ export class OpenIDAuthDriver extends BaseOAuthDriver {
75
92
  const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
76
93
  const paramsConfig = typeof this.config['params'] === 'object' ? this.config['params'] : {};
77
94
  const params = {
95
+ ...additionalParams,
78
96
  scope: this.config['scope'] ?? 'openid profile email',
79
97
  access_type: 'offline',
80
98
  ...paramsConfig,
@@ -84,7 +102,6 @@ export class OpenIDAuthDriver extends BaseOAuthDriver {
84
102
  state: codeChallenge,
85
103
  nonce: codeChallenge,
86
104
  redirect_uri: this.redirectUrl,
87
- ...additionalParams,
88
105
  };
89
106
  if (prompt) {
90
107
  params.prompt = 'consent';
@@ -155,7 +172,7 @@ export function createOpenIDAuthRouter(providerName) {
155
172
  issuer: 'directus',
156
173
  });
157
174
  const additionalParams = Object.fromEntries(Object.entries(req.query)
158
- .filter(([k, v]) => k !== 'prompt' && k !== 'redirect' && typeof v === 'string')
175
+ .filter(([k, v]) => RESERVED_AUTH_PARAMS.has(k) === false && typeof v === 'string')
159
176
  .map(([k, v]) => [k, v]));
160
177
  const authUrl = await provider.generateAuthUrl(codeVerifier, prompt, additionalParams);
161
178
  const urlParams = new URL(authUrl);
@@ -0,0 +1 @@
1
+ export {};
@@ -254,6 +254,9 @@ CORS_CREDENTIALS=true
254
254
  # Value for the Access-Control-Max-Age header [18000]
255
255
  CORS_MAX_AGE=18000
256
256
 
257
+ # Value for the Cross-Origin-Opener-Policy header: same-origin, same-origin-allow-popups or unsafe-none ["same-origin"]
258
+ # CROSS_ORIGIN_OPENER_POLICY="same-origin"
259
+
257
260
  ####################################################################################################
258
261
  ### Argon2
259
262
 
package/dist/env.js CHANGED
@@ -63,6 +63,7 @@ const allowedEnvironmentVars = [
63
63
  'IMPORT_IP_DENY_LIST',
64
64
  'CONTENT_SECURITY_POLICY_.+',
65
65
  'HSTS_.+',
66
+ 'CROSS_ORIGIN_OPENER_POLICY',
66
67
  // hashing
67
68
  'HASH_.+',
68
69
  'X_POWERED_BY_ENABLED',
@@ -283,6 +284,7 @@ const defaults = {
283
284
  IP_TRUST_PROXY: true,
284
285
  IP_CUSTOM_HEADER: false,
285
286
  IMPORT_IP_DENY_LIST: ['0.0.0.0', '169.254.169.254'],
287
+ CROSS_ORIGIN_OPENER_POLICY: 'same-origin',
286
288
  SERVE_APP: true,
287
289
  SERVE_GRAPHQL: true,
288
290
  RELATIONAL_BATCH_SIZE: 25000,
@@ -4,7 +4,9 @@ export default defineOperationApi({
4
4
  handler: ({ filter }, { data }) => {
5
5
  const errors = validatePayload(filter, data, { requireAll: true });
6
6
  if (errors.length > 0) {
7
- throw errors;
7
+ // Joi errors hold the validated data ($env, $accountability…) in _original, details[].context and in some
8
+ // messages: only keep the path and type, both derived from the filter, as webhook flows can return them
9
+ throw errors.flatMap((error) => error.details.map(({ path, type }) => ({ path, type })));
8
10
  }
9
11
  else {
10
12
  return null;
@@ -1,9 +1,13 @@
1
1
  import { toArray } from '@wbce-d9/utils';
2
+ import { URL } from 'url';
2
3
  import env from '../env.js';
3
4
  import isUrlAllowed from './is-url-allowed.js';
4
- function isUrlValid(string) {
5
+ // Sentinel origin that never resolves (RFC 6761): relative paths keep it,
6
+ // anything pointing elsewhere (`//x`, `/\x`, `https://x`) does not.
7
+ const RELATIVE_BASE = 'https://relative.invalid';
8
+ function isUrlValid(url, base) {
5
9
  try {
6
- new URL(string);
10
+ new URL(url, base);
7
11
  return true;
8
12
  }
9
13
  catch (err) {
@@ -18,25 +22,20 @@ export function isRedirectAllowedOnLogin(redirect, provider) {
18
22
  return true; // empty redirect
19
23
  if (typeof redirect !== 'string')
20
24
  return false; // invalid type
21
- const publicUrl = env['PUBLIC_URL'];
22
25
  if (!isUrlValid(redirect)) {
23
- if (!redirect.startsWith('//')) {
24
- // should be a relative path like `/admin/test`
25
- return true;
26
- }
27
- // domain without protocol `//example.com/test`
28
- return false;
26
+ // relative path like `/admin/test`, allowed unless it resolves to another origin
27
+ return isUrlValid(redirect, RELATIVE_BASE) && new URL(redirect, RELATIVE_BASE).origin === RELATIVE_BASE;
29
28
  }
30
- const { protocol: redirectProtocol, hostname: redirectDomain } = new URL(redirect);
29
+ const { protocol: redirectProtocol, host: redirectHost } = new URL(redirect);
30
+ const publicUrl = env['PUBLIC_URL'];
31
31
  const envKey = `AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`;
32
32
  if (envKey in env) {
33
33
  if (isUrlAllowed(redirect, [...toArray(env[envKey]), publicUrl]))
34
34
  return true;
35
35
  }
36
- if (!isUrlValid(publicUrl)) {
36
+ if (!isUrlValid(publicUrl))
37
37
  return false;
38
- }
39
38
  // allow redirects to the defined PUBLIC_URL
40
- const { protocol: publicProtocol, hostname: publicDomain } = new URL(publicUrl);
41
- return `${redirectProtocol}//${redirectDomain}` === `${publicProtocol}//${publicDomain}`;
39
+ const { protocol: publicProtocol, host: publicHost } = new URL(publicUrl);
40
+ return redirectProtocol === publicProtocol && redirectHost === publicHost;
42
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbce-d9/api",
3
- "version": "12.5.9",
3
+ "version": "12.5.11",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -143,20 +143,20 @@
143
143
  "tmp-promise": "3.0.3",
144
144
  "tsx": "3.12.6",
145
145
  "uuid-validate": "0.0.3",
146
+ "@wbce-d9/constants": "10.0.0",
146
147
  "@wbce-d9/app": "11.0.0",
147
148
  "@wbce-d9/exceptions": "10.0.0",
148
149
  "@wbce-d9/specs": "9.26.5",
149
150
  "@wbce-d9/schema": "10.0.0",
150
- "@wbce-d9/extensions-sdk": "10.0.4",
151
151
  "@wbce-d9/storage": "10.0.0",
152
- "@wbce-d9/constants": "10.0.0",
153
- "@wbce-d9/storage-driver-s3": "10.0.3",
154
- "@wbce-d9/storage-driver-azure": "10.0.3",
155
- "@wbce-d9/update-check": "10.0.4",
156
- "@wbce-d9/utils": "10.0.3",
157
152
  "@wbce-d9/storage-driver-gcs": "10.0.3",
158
153
  "@wbce-d9/storage-driver-cloudinary": "10.0.4",
159
- "@wbce-d9/storage-driver-local": "10.0.3"
154
+ "@wbce-d9/storage-driver-azure": "10.0.3",
155
+ "@wbce-d9/storage-driver-local": "10.0.3",
156
+ "@wbce-d9/storage-driver-s3": "10.0.3",
157
+ "@wbce-d9/extensions-sdk": "10.0.4",
158
+ "@wbce-d9/update-check": "10.0.4",
159
+ "@wbce-d9/utils": "10.0.3"
160
160
  },
161
161
  "devDependencies": {
162
162
  "@ngneat/falso": "6.4.0",