8
8
//-----------------------------------------------------------------------------
9
9
10
10
// the methods allowed for simple requests
11
- export const corsSafeMethods = new Set ( [ "GET" , "HEAD" , "POST" ] ) ;
11
+ export const safeMethods = new Set ( [ "GET" , "HEAD" , "POST" ] ) ;
12
12
13
13
// the headers allowed for simple requests
14
- export const corsSafeHeaders = new Set ( [
14
+ export const safeRequestHeaders = new Set ( [
15
15
"accept" ,
16
16
"accept-language" ,
17
17
"content-language" ,
18
18
"content-type" ,
19
19
"range" ,
20
20
] ) ;
21
21
22
+ // the headers that are forbidden to be sent with requests
23
+ export const forbiddenRequestHeaders = new Set ( [
24
+ "accept-charset" ,
25
+ "accept-encoding" ,
26
+ "access-control-request-headers" ,
27
+ "access-control-request-method" ,
28
+ "connection" ,
29
+ "content-length" ,
30
+ "cookie" ,
31
+ "cookie2" ,
32
+ "date" ,
33
+ "dnt" ,
34
+ "expect" ,
35
+ "host" ,
36
+ "keep-alive" ,
37
+ "origin" ,
38
+ "referer" ,
39
+ "te" ,
40
+ "trailer" ,
41
+ "transfer-encoding" ,
42
+ "upgrade" ,
43
+ "user-agent" ,
44
+ "via" ,
45
+ ] ) ;
46
+
47
+ // the headers that can be used to override the method
48
+ const methodOverrideRequestHeaders = new Set ( [
49
+ "x-http-method" ,
50
+ "x-http-method-override" ,
51
+ "x-method-override" ,
52
+ ] ) ;
53
+
54
+ // the headers that are always allowed to be read from responses
55
+ export const safeResponseHeaders = new Set ( [
56
+ "cache-control" ,
57
+ "content-language" ,
58
+ "content-type" ,
59
+ "expires" ,
60
+ "last-modified" ,
61
+ "pragma" ,
62
+ ] ) ;
63
+
64
+ // the headers that are forbidden to be read from responses
65
+ export const forbiddenResponseHeaders = new Set ( [ "set-cookie" , "set-cookie2" ] ) ;
66
+
22
67
// the content types allowed for simple requests
23
- const corsSimpleContentTypes = new Set ( [
68
+ const simpleRequestContentTypes = new Set ( [
24
69
"application/x-www-form-urlencoded" ,
25
70
"multipart/form-data" ,
26
71
"text/plain" ,
27
72
] ) ;
28
73
74
+ // the methods that are forbidden to be used with CORS
75
+ const forbiddenMethods = new Set ( [ "CONNECT" , "TRACE" , "TRACK" ] ) ;
76
+
29
77
export const CORS_ALLOW_ORIGIN = "Access-Control-Allow-Origin" ;
30
78
export const CORS_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials" ;
31
79
export const CORS_EXPOSE_HEADERS = "Access-Control-Expose-Headers" ;
@@ -40,6 +88,37 @@ export const CORS_ORIGIN = "Origin";
40
88
// Helpers
41
89
//-----------------------------------------------------------------------------
42
90
91
+ /**
92
+ * Checks if a method is forbidden for CORS.
93
+ * @param {string } header The header to check.
94
+ * @param {string } value The value to check.
95
+ * @returns {boolean } `true` if the method is forbidden, `false` otherwise.
96
+ * @see https://fetch.spec.whatwg.org/#forbidden-method
97
+ */
98
+ function isForbiddenMethodOverride ( header , value ) {
99
+ return (
100
+ methodOverrideRequestHeaders . has ( header ) &&
101
+ forbiddenMethods . has ( value . toUpperCase ( ) )
102
+ ) ;
103
+ }
104
+
105
+ /**
106
+ * Checks if a request header is forbidden for CORS.
107
+ * @param {string } header The header to check.
108
+ * @param {string } value The value to check.
109
+ * @returns {boolean } `true` if the header is forbidden, `false` otherwise.
110
+ * @see https://fetch.spec.whatwg.org/#forbidden-header-name
111
+ */
112
+ function isForbiddenRequestHeader ( header , value ) {
113
+ // eslint-disable-line no-unused-vars
114
+ return (
115
+ forbiddenRequestHeaders . has ( header ) ||
116
+ header . startsWith ( "proxy-" ) ||
117
+ header . startsWith ( "sec-" ) ||
118
+ isForbiddenMethodOverride ( header , value )
119
+ ) ;
120
+ }
121
+
43
122
/**
44
123
* Checks if a Range header value is a simple range according to the Fetch API spec.
45
124
* @see https://fetch.spec.whatwg.org/#http-headers
@@ -91,21 +170,22 @@ function isSimpleRangeHeader(range) {
91
170
* Represents an error that occurs when a CORS request is blocked.
92
171
*/
93
172
export class CorsError extends Error {
94
-
95
173
/**
96
174
* The name of the error.
97
175
* @type {string }
98
176
*/
99
177
name = "CorsError" ;
100
-
178
+
101
179
/**
102
180
* Creates a new instance.
103
181
* @param {string } requestUrl The URL of the request.
104
182
* @param {string } origin The origin of the client making the request.
105
183
* @param {string } message The error message.
106
184
*/
107
185
constructor ( requestUrl , origin , message ) {
108
- super ( `Access to fetch at '${ requestUrl } ' from origin '${ origin } ' has been blocked by CORS policy: ${ message } ` ) ;
186
+ super (
187
+ `Access to fetch at '${ requestUrl } ' from origin '${ origin } ' has been blocked by CORS policy: ${ message } ` ,
188
+ ) ;
109
189
}
110
190
}
111
191
@@ -123,7 +203,7 @@ export function assertCorsResponse(response, origin) {
123
203
throw new CorsError (
124
204
response . url ,
125
205
origin ,
126
- "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource."
206
+ "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource." ,
127
207
) ;
128
208
}
129
209
@@ -134,12 +214,45 @@ export function assertCorsResponse(response, origin) {
134
214
`The 'Access-Control-Allow-Origin' header has a value '${ originHeader } ' that is not equal to the supplied origin.` ,
135
215
) ;
136
216
}
217
+ }
137
218
219
+ /**
220
+ * Processes a CORS response to ensure it's valid and doesn't contain
221
+ * any forbidden headers.
222
+ * @param {Response } response The response to process.
223
+ * @param {string } origin The origin of the request.
224
+ * @returns {Response } The processed response.
225
+ */
226
+ export function processCorsResponse ( response , origin ) {
227
+ // first check that the response is allowed
228
+ assertCorsResponse ( response , origin ) ;
229
+
230
+ // check if the Access-Control-Expose-Headers header is present
138
231
const exposedHeaders = response . headers . get ( CORS_EXPOSE_HEADERS ) ;
232
+ const allowedHeaders = exposedHeaders
233
+ ? new Set ( exposedHeaders . toLowerCase ( ) . split ( ", " ) )
234
+ : new Set ( ) ;
235
+
236
+ // next filter out any headers that aren't allowed
237
+ for ( const key of response . headers . keys ( ) ) {
238
+ // first check if the header is always allowed
239
+ if ( safeResponseHeaders . has ( key ) ) {
240
+ continue ;
241
+ }
139
242
140
- if ( exposedHeaders ) {
141
- throw new Error ( "Access-Control-Expose-Headers is not yet supported." ) ;
243
+ // next check if the header is never allowed
244
+ if ( forbiddenResponseHeaders . has ( key ) ) {
245
+ response . headers . delete ( key ) ;
246
+ continue ;
247
+ }
248
+
249
+ // finally check if the header is allowed by the server
250
+ if ( ! allowedHeaders . has ( key ) ) {
251
+ response . headers . delete ( key ) ;
252
+ }
142
253
}
254
+
255
+ return response ;
143
256
}
144
257
145
258
/**
@@ -149,23 +262,23 @@ export function assertCorsResponse(response, origin) {
149
262
*/
150
263
export function isCorsSimpleRequest ( request ) {
151
264
// if it's not a simple method then it's not a simple request
152
- if ( ! corsSafeMethods . has ( request . method ) ) {
265
+ if ( ! safeMethods . has ( request . method ) ) {
153
266
return false ;
154
267
}
155
268
156
269
// check all headers to ensure they're allowed
157
270
const headers = request . headers ;
158
271
159
272
for ( const header of headers . keys ( ) ) {
160
- if ( ! corsSafeHeaders . has ( header ) ) {
273
+ if ( ! safeRequestHeaders . has ( header ) ) {
161
274
return false ;
162
275
}
163
276
}
164
277
165
278
// check the content type
166
279
const contentType = headers . get ( "content-type" ) ;
167
280
168
- if ( contentType && ! corsSimpleContentTypes . has ( contentType ) ) {
281
+ if ( contentType && ! simpleRequestContentTypes . has ( contentType ) ) {
169
282
return false ;
170
283
}
171
284
@@ -213,12 +326,6 @@ export class CorsPreflightData {
213
326
*/
214
327
allowCredentials = false ;
215
328
216
- /**
217
- * The exposed headers for this URL.
218
- * @type {Set<string> }
219
- */
220
- exposedHeaders = new Set ( ) ;
221
-
222
329
/**
223
330
* The maximum age for this URL.
224
331
* @type {number }
@@ -248,14 +355,9 @@ export class CorsPreflightData {
248
355
249
356
this . allowCredentials = headers . get ( CORS_ALLOW_CREDENTIALS ) === "true" ;
250
357
251
- const exposeHeaders = headers . get ( CORS_EXPOSE_HEADERS ) ;
252
- if ( exposeHeaders ) {
253
- this . exposedHeaders = new Set (
254
- exposeHeaders . toLowerCase ( ) . split ( ", " ) ,
255
- ) ;
256
- }
257
-
258
358
this . maxAge = Number ( headers . get ( CORS_MAX_AGE ) ) || Infinity ;
359
+
360
+ // Note: Access-Control-Expose-Headers is not honored on preflight requests
259
361
}
260
362
261
363
/**
@@ -266,12 +368,11 @@ export class CorsPreflightData {
266
368
* @throws {Error } When the method is not allowed.
267
369
*/
268
370
#validateMethod( request , origin ) {
269
-
270
371
const method = request . method . toUpperCase ( ) ;
271
-
372
+
272
373
if (
273
374
! this . allowAllMethods &&
274
- ! corsSafeMethods . has ( method ) &&
375
+ ! safeMethods . has ( method ) &&
275
376
! this . allowedMethods . has ( method )
276
377
) {
277
378
throw new CorsError (
@@ -290,12 +391,11 @@ export class CorsPreflightData {
290
391
* @throws {Error } When the headers are not allowed.
291
392
*/
292
393
#validateHeaders( request , origin ) {
293
-
294
394
const { headers } = request ;
295
-
395
+
296
396
for ( const header of headers . keys ( ) ) {
297
397
// simple headers are always allowed
298
- if ( corsSafeHeaders . has ( header ) ) {
398
+ if ( safeRequestHeaders . has ( header ) ) {
299
399
continue ;
300
400
}
301
401
0 commit comments