@tekir/cors 0.1.4 → 0.1.6

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.
Files changed (3) hide show
  1. package/dist/cors.js +35 -13
  2. package/package.json +1 -1
  3. package/src/cors.ts +37 -12
package/dist/cors.js CHANGED
@@ -42,19 +42,24 @@ const defaults = {
42
42
  */
43
43
  export function cors(userConfig = {}) {
44
44
  const cfg = { ...defaults, ...userConfig };
45
+ // Reflecting an arbitrary Origin with `Access-Control-Allow-Credentials: true`
46
+ // hands any site credentialed access to this origin's responses. Refuse the
47
+ // dangerous `origin: true` + `credentials: true` combination at construction
48
+ // time and require an explicit allowlist (string/array/function) instead.
49
+ if (cfg.credentials && cfg.origin === true) {
50
+ throw new Error('@tekir/cors: `credentials: true` cannot be combined with `origin: true`. ' +
51
+ 'Reflecting every Origin with credentials enabled lets any site read authenticated responses. ' +
52
+ 'Provide an explicit allowlist (string, string[], or a validator function).');
53
+ }
45
54
  return async (ctx, next) => {
46
55
  if (!cfg.enabled)
47
56
  return next();
48
57
  const origin = ctx.request.header('origin') || ctx.headers?.origin || '';
49
58
  let allowOrigin = '';
50
59
  if (cfg.origin === true) {
51
- // When credentials are enabled, never use wildcard, reflect the request origin instead.
52
- if (cfg.credentials && origin) {
53
- allowOrigin = origin;
54
- }
55
- else {
56
- allowOrigin = origin || '*';
57
- }
60
+ // No credentials here (the credentials+true combo is rejected above), so
61
+ // the wildcard is safe. Reflect the request origin when present, else `*`.
62
+ allowOrigin = origin || '*';
58
63
  }
59
64
  else if (cfg.origin === false) {
60
65
  allowOrigin = '';
@@ -63,12 +68,16 @@ export function cors(userConfig = {}) {
63
68
  allowOrigin = cfg.origin;
64
69
  }
65
70
  else if (Array.isArray(cfg.origin)) {
66
- const lowerOrigin = origin.toLowerCase();
67
- allowOrigin = cfg.origin.some(o => o.toLowerCase() === lowerOrigin) ? origin : '';
71
+ // RFC 6454 origins are compared exactly (case-sensitive scheme/host).
72
+ allowOrigin = cfg.origin.includes(origin) ? origin : '';
68
73
  }
69
74
  else if (typeof cfg.origin === 'function') {
70
75
  allowOrigin = cfg.origin(origin) ? origin : '';
71
76
  }
77
+ // A `null` origin (sandboxed iframes, data:/file: schemes) must never be
78
+ // trusted with credentials — it is not bound to any real site.
79
+ if (cfg.credentials && allowOrigin === 'null')
80
+ return next();
72
81
  if (!allowOrigin)
73
82
  return next();
74
83
  // Stash the negotiated CORS headers on ctx so the framework merges them
@@ -92,10 +101,23 @@ export function cors(userConfig = {}) {
92
101
  }
93
102
  const method = ctx.request?.method || ctx.request?.raw?.method || '';
94
103
  if (method === 'OPTIONS') {
95
- headers.set('Access-Control-Allow-Methods', (cfg.methods || []).join(', '));
96
- headers.set('Access-Control-Allow-Headers', cfg.headers === true
97
- ? ctx.request.header('access-control-request-headers') || '*'
98
- : Array.isArray(cfg.headers) ? cfg.headers.join(', ') : '');
104
+ // Skip empty header values: an empty Allow-Methods/Allow-Headers silently
105
+ // breaks the preflight instead of leaving the browser's defaults in place.
106
+ if (cfg.methods?.length)
107
+ headers.set('Access-Control-Allow-Methods', cfg.methods.join(', '));
108
+ let allowHeaders = '';
109
+ if (cfg.headers === true) {
110
+ // `headers: true` reflects the requested headers. With credentials we
111
+ // must echo the explicit list (a `*` is invalid for credentialed
112
+ // requests and would fail the preflight), never a wildcard.
113
+ const requested = ctx.request.header('access-control-request-headers') || '';
114
+ allowHeaders = cfg.credentials ? requested : (requested || '*');
115
+ }
116
+ else if (Array.isArray(cfg.headers)) {
117
+ allowHeaders = cfg.headers.join(', ');
118
+ }
119
+ if (allowHeaders)
120
+ headers.set('Access-Control-Allow-Headers', allowHeaders);
99
121
  if (cfg.maxAge)
100
122
  headers.set('Access-Control-Max-Age', String(cfg.maxAge));
101
123
  // Short-circuit the chain. Framework merges $responseHeaders onto this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekir/cors",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CORS middleware for cross-origin requests",
5
5
  "author": "dev@tekir.io",
6
6
  "license": "MIT",
package/src/cors.ts CHANGED
@@ -46,6 +46,18 @@ const defaults: CorsConfig = {
46
46
  export function cors(userConfig: CorsConfig = {}) {
47
47
  const cfg = { ...defaults, ...userConfig }
48
48
 
49
+ // Reflecting an arbitrary Origin with `Access-Control-Allow-Credentials: true`
50
+ // hands any site credentialed access to this origin's responses. Refuse the
51
+ // dangerous `origin: true` + `credentials: true` combination at construction
52
+ // time and require an explicit allowlist (string/array/function) instead.
53
+ if (cfg.credentials && cfg.origin === true) {
54
+ throw new Error(
55
+ '@tekir/cors: `credentials: true` cannot be combined with `origin: true`. ' +
56
+ 'Reflecting every Origin with credentials enabled lets any site read authenticated responses. ' +
57
+ 'Provide an explicit allowlist (string, string[], or a validator function).'
58
+ )
59
+ }
60
+
49
61
  return async (ctx: any, next: () => Promise<void>) => {
50
62
  if (!cfg.enabled) return next()
51
63
 
@@ -53,23 +65,24 @@ export function cors(userConfig: CorsConfig = {}) {
53
65
 
54
66
  let allowOrigin = ''
55
67
  if (cfg.origin === true) {
56
- // When credentials are enabled, never use wildcard, reflect the request origin instead.
57
- if (cfg.credentials && origin) {
58
- allowOrigin = origin
59
- } else {
60
- allowOrigin = origin || '*'
61
- }
68
+ // No credentials here (the credentials+true combo is rejected above), so
69
+ // the wildcard is safe. Reflect the request origin when present, else `*`.
70
+ allowOrigin = origin || '*'
62
71
  } else if (cfg.origin === false) {
63
72
  allowOrigin = ''
64
73
  } else if (typeof cfg.origin === 'string') {
65
74
  allowOrigin = cfg.origin
66
75
  } else if (Array.isArray(cfg.origin)) {
67
- const lowerOrigin = origin.toLowerCase()
68
- allowOrigin = cfg.origin.some(o => o.toLowerCase() === lowerOrigin) ? origin : ''
76
+ // RFC 6454 origins are compared exactly (case-sensitive scheme/host).
77
+ allowOrigin = cfg.origin.includes(origin) ? origin : ''
69
78
  } else if (typeof cfg.origin === 'function') {
70
79
  allowOrigin = cfg.origin(origin) ? origin : ''
71
80
  }
72
81
 
82
+ // A `null` origin (sandboxed iframes, data:/file: schemes) must never be
83
+ // trusted with credentials — it is not bound to any real site.
84
+ if (cfg.credentials && allowOrigin === 'null') return next()
85
+
73
86
  if (!allowOrigin) return next()
74
87
 
75
88
  // Stash the negotiated CORS headers on ctx so the framework merges them
@@ -91,10 +104,22 @@ export function cors(userConfig: CorsConfig = {}) {
91
104
 
92
105
  const method = ctx.request?.method || ctx.request?.raw?.method || ''
93
106
  if (method === 'OPTIONS') {
94
- headers.set('Access-Control-Allow-Methods', (cfg.methods || []).join(', '))
95
- headers.set('Access-Control-Allow-Headers', cfg.headers === true
96
- ? ctx.request.header('access-control-request-headers') || '*'
97
- : Array.isArray(cfg.headers) ? cfg.headers.join(', ') : '')
107
+ // Skip empty header values: an empty Allow-Methods/Allow-Headers silently
108
+ // breaks the preflight instead of leaving the browser's defaults in place.
109
+ if (cfg.methods?.length) headers.set('Access-Control-Allow-Methods', cfg.methods.join(', '))
110
+
111
+ let allowHeaders = ''
112
+ if (cfg.headers === true) {
113
+ // `headers: true` reflects the requested headers. With credentials we
114
+ // must echo the explicit list (a `*` is invalid for credentialed
115
+ // requests and would fail the preflight), never a wildcard.
116
+ const requested = ctx.request.header('access-control-request-headers') || ''
117
+ allowHeaders = cfg.credentials ? requested : (requested || '*')
118
+ } else if (Array.isArray(cfg.headers)) {
119
+ allowHeaders = cfg.headers.join(', ')
120
+ }
121
+ if (allowHeaders) headers.set('Access-Control-Allow-Headers', allowHeaders)
122
+
98
123
  if (cfg.maxAge) headers.set('Access-Control-Max-Age', String(cfg.maxAge))
99
124
  // Short-circuit the chain. Framework merges $responseHeaders onto this
100
125
  // bare 204, so the preflight response carries all the negotiated bits.