Skip to content

Commit a604e46

Browse files
authored
fix: Limit Access-Control-Request-Headers to unsafe headers only (#53)
1 parent 7931479 commit a604e46

File tree

4 files changed

+304
-57
lines changed

4 files changed

+304
-57
lines changed

src/cors.js

+38
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,44 @@ export function validateCorsRequest(request, origin) {
391391
}
392392
}
393393

394+
/**
395+
* Gets an array of headers that are not allowed in a CORS simple request.
396+
* @param {Request} request The request to check.
397+
* @returns {string[]} Array of header names that are not simple headers.
398+
*/
399+
export function getNonSimpleHeaders(request) {
400+
const result = [];
401+
const headers = request.headers;
402+
403+
for (const header of headers.keys()) {
404+
405+
// Range header needs special validation
406+
if (header === "range") {
407+
const rangeValue = headers.get(header);
408+
if (rangeValue && !isSimpleRangeHeader(rangeValue)) {
409+
result.push(header);
410+
}
411+
continue;
412+
}
413+
414+
// Content-Type header needs special validation
415+
if (header === "content-type") {
416+
const contentType = headers.get(header);
417+
if (contentType && !simpleRequestContentTypes.has(contentType)) {
418+
result.push(header);
419+
}
420+
continue;
421+
}
422+
423+
// Check if header is in the safe list
424+
if (!safeRequestHeaders.has(header)) {
425+
result.push(header);
426+
}
427+
}
428+
429+
return result;
430+
}
431+
394432
/**
395433
* A class for storing CORS preflight data.
396434
*/

src/fetch-mocker.js

+19-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
CORS_REQUEST_HEADERS,
1919
CORS_ORIGIN,
2020
CorsError,
21+
getNonSimpleHeaders,
2122
} from "./cors.js";
2223
import { createCustomRequest } from "./custom-request.js";
2324

@@ -349,21 +350,34 @@ export class FetchMocker {
349350
* @param {Request} request The request to create a preflight request for.
350351
* @returns {Request} The preflight request.
351352
* @throws {Error} When there is no base URL.
353+
* @see https://fetch.spec.whatwg.org/#cors-preflight-fetch
352354
*/
353355
#createPreflightRequest(request) {
354356
if (!this.#baseUrl) {
355357
throw new Error(
356358
"Cannot create preflight request without a base URL.",
357359
);
358360
}
361+
362+
const nonsimpleHeaders = getNonSimpleHeaders(request);
363+
364+
/** @type {Record<string,string>} */
365+
const headers = {
366+
Accept: "*/*",
367+
[CORS_REQUEST_METHOD]: request.method,
368+
[CORS_ORIGIN]: this.#baseUrl.origin,
369+
};
370+
371+
if (nonsimpleHeaders.length > 0) {
372+
headers[CORS_REQUEST_HEADERS] = nonsimpleHeaders.join(",");
373+
}
359374

360375
return new this.#Request(request.url, {
361376
method: "OPTIONS",
362-
headers: {
363-
[CORS_REQUEST_METHOD]: request.method,
364-
[CORS_REQUEST_HEADERS]: [...request.headers.keys()].join(","),
365-
[CORS_ORIGIN]: this.#baseUrl.origin,
366-
},
377+
headers,
378+
mode: "cors",
379+
referrer: request.referrer,
380+
referrerPolicy: request.referrerPolicy,
367381
});
368382
}
369383

tests/cors.test.js

+77-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//-----------------------------------------------------------------------------
1010

1111
import assert from "node:assert";
12-
import { isCorsSimpleRequest } from "../src/cors.js";
12+
import { isCorsSimpleRequest, getNonSimpleHeaders } from "../src/cors.js";
1313

1414
//-----------------------------------------------------------------------------
1515
// Tests
@@ -175,4 +175,80 @@ describe("http", () => {
175175
});
176176
});
177177
});
178+
179+
describe("getNonSimpleHeaders()", () => {
180+
it("should return empty array for request with no headers", () => {
181+
const request = new Request("https://example.com");
182+
assert.deepStrictEqual(getNonSimpleHeaders(request), []);
183+
});
184+
185+
it("should return empty array for request with only simple headers", () => {
186+
const headers = new Headers({
187+
Accept: "application/json",
188+
"Accept-Language": "en-US",
189+
"Content-Language": "en-US"
190+
});
191+
const request = new Request("https://example.com", { headers });
192+
assert.deepStrictEqual(getNonSimpleHeaders(request), []);
193+
});
194+
195+
it("should return array containing non-simple headers", () => {
196+
const headers = new Headers({
197+
Accept: "application/json",
198+
Authorization: "Bearer token",
199+
"X-Custom-Header": "value"
200+
});
201+
const request = new Request("https://example.com", { headers });
202+
assert.deepStrictEqual(
203+
getNonSimpleHeaders(request),
204+
["authorization", "x-custom-header"]
205+
);
206+
});
207+
208+
it("should identify non-simple content-type header", () => {
209+
const headers = new Headers({
210+
"Content-Type": "application/json"
211+
});
212+
const request = new Request("https://example.com", { headers });
213+
assert.deepStrictEqual(getNonSimpleHeaders(request), ["content-type"]);
214+
});
215+
216+
it("should not include simple content-type header", () => {
217+
const headers = new Headers({
218+
"Content-Type": "text/plain"
219+
});
220+
const request = new Request("https://example.com", { headers });
221+
assert.deepStrictEqual(getNonSimpleHeaders(request), []);
222+
});
223+
224+
it("should identify invalid range header", () => {
225+
const headers = new Headers({
226+
Range: "bytes=0-1024,2048-3072"
227+
});
228+
const request = new Request("https://example.com", { headers });
229+
assert.deepStrictEqual(getNonSimpleHeaders(request), ["range"]);
230+
});
231+
232+
it("should not include valid range header", () => {
233+
const headers = new Headers({
234+
Range: "bytes=0-1024"
235+
});
236+
const request = new Request("https://example.com", { headers });
237+
assert.deepStrictEqual(getNonSimpleHeaders(request), []);
238+
});
239+
240+
it("should identify multiple non-simple headers", () => {
241+
const headers = new Headers({
242+
Authorization: "Bearer token",
243+
"Content-Type": "application/json",
244+
Range: "bytes=0-1024,2048-3072",
245+
"X-Custom-Header": "value"
246+
});
247+
const request = new Request("https://example.com", { headers });
248+
assert.deepStrictEqual(
249+
getNonSimpleHeaders(request),
250+
["authorization", "content-type", "range", "x-custom-header"]
251+
);
252+
});
253+
});
178254
});

0 commit comments

Comments
 (0)