Skip to content

Commit e472e14

Browse files
authored
fix: Ensure forbidden CORS headers throw an error (#51)
fixes #40
1 parent 0b6b981 commit e472e14

File tree

3 files changed

+125
-1
lines changed

3 files changed

+125
-1
lines changed

src/cors.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const simpleRequestContentTypes = new Set([
7272
]);
7373

7474
// the methods that are forbidden to be used with CORS
75-
const forbiddenMethods = new Set(["CONNECT", "TRACE", "TRACK"]);
75+
export const forbiddenMethods = new Set(["CONNECT", "TRACE", "TRACK"]);
7676

7777
export const CORS_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
7878
export const CORS_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
@@ -359,6 +359,38 @@ export function isCorsSimpleRequest(request) {
359359
return true;
360360
}
361361

362+
/**
363+
* Validates a CORS request.
364+
* @param {Request} request The request to validate.
365+
* @param {string} origin The origin of the request.
366+
* @returns {void}
367+
* @throws {CorsError} When the request is not allowed.
368+
*/
369+
export function validateCorsRequest(request, origin) {
370+
// check the method
371+
if (forbiddenMethods.has(request.method)) {
372+
throw new CorsError(
373+
request.url,
374+
origin,
375+
`Method ${request.method} is not allowed.`,
376+
);
377+
}
378+
379+
// check the headers
380+
for (const header of request.headers.keys()) {
381+
382+
const value = /** @type {string} */ (request.headers.get(header));
383+
384+
if (isForbiddenRequestHeader(header, value)) {
385+
throw new CorsError(
386+
request.url,
387+
origin,
388+
`Header ${header} is not allowed.`,
389+
);
390+
}
391+
}
392+
}
393+
362394
/**
363395
* A class for storing CORS preflight data.
364396
*/

src/fetch-mocker.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
CorsPreflightData,
1414
assertCorsResponse,
1515
processCorsResponse,
16+
validateCorsRequest,
1617
CORS_REQUEST_METHOD,
1718
CORS_REQUEST_HEADERS,
1819
CORS_ORIGIN,
@@ -228,6 +229,8 @@ export class FetchMocker {
228229
useCors = true;
229230
const includeCredentials =
230231
request.credentials === "include";
232+
233+
validateCorsRequest(request, this.#baseUrl.origin);
231234

232235
if (isCorsSimpleRequest(request)) {
233236
if (includeCredentials) {

tests/fetch-mocker.test.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//-----------------------------------------------------------------------------
1111

1212
import assert from "node:assert";
13+
import { forbiddenMethods, forbiddenRequestHeaders } from "../src/cors.js";
1314
import { MockServer } from "../src/mock-server.js";
1415
import { FetchMocker } from "../src/fetch-mocker.js";
1516
import { CookieCredentials } from "../src/cookie-credentials.js";
@@ -1949,6 +1950,94 @@ describe("FetchMocker", () => {
19491950
});
19501951
});
19511952
});
1953+
1954+
describe("Forbidden headers", () => {
1955+
1956+
[...forbiddenRequestHeaders].forEach(header => {
1957+
it(`should throw an error when the header is ${header}`, async () => {
1958+
const server = new MockServer(API_URL);
1959+
const fetchMocker = new FetchMocker({
1960+
servers: [server],
1961+
baseUrl: ALT_BASE_URL,
1962+
});
1963+
const url = new URL("/hello", API_URL);
1964+
1965+
await assert.rejects(
1966+
fetchMocker.fetch(url, {
1967+
headers: {
1968+
[header]: "Foo",
1969+
},
1970+
}),
1971+
new RegExp(`Header ${header} is not allowed`, "iu"),
1972+
);
1973+
});
1974+
});
1975+
1976+
it("should throw an error when the header begins with sec-", async () => {
1977+
const server = new MockServer(API_URL);
1978+
const fetchMocker = new FetchMocker({
1979+
servers: [server],
1980+
baseUrl: ALT_BASE_URL,
1981+
});
1982+
const url = new URL("/hello", API_URL);
1983+
1984+
await assert.rejects(
1985+
fetchMocker.fetch(url, {
1986+
headers: {
1987+
"sec-foo": "Foo",
1988+
},
1989+
}),
1990+
new RegExp("Header sec-foo is not allowed", "iu"),
1991+
);
1992+
});
1993+
1994+
it("should throw an error when the header begins with proxy-", async () => {
1995+
const server = new MockServer(API_URL);
1996+
const fetchMocker = new FetchMocker({
1997+
servers: [server],
1998+
baseUrl: ALT_BASE_URL,
1999+
});
2000+
const url = new URL("/hello", API_URL);
2001+
2002+
await assert.rejects(
2003+
fetchMocker.fetch(url, {
2004+
headers: {
2005+
"proxy-foo": "Foo",
2006+
},
2007+
}),
2008+
new RegExp("Header proxy-foo is not allowed", "iu"),
2009+
);
2010+
});
2011+
2012+
[
2013+
"X-Http-Method",
2014+
"X-Http-Method-Override",
2015+
"X-Method-Override",
2016+
].forEach(header => {
2017+
[...forbiddenMethods].forEach(method => {
2018+
2019+
it(`should throw an error when the ${header} header is ${method}`, async () => {
2020+
const server = new MockServer(API_URL);
2021+
const fetchMocker = new FetchMocker({
2022+
servers: [server],
2023+
baseUrl: ALT_BASE_URL,
2024+
});
2025+
const url = new URL("/hello", API_URL);
2026+
2027+
await assert.rejects(
2028+
fetchMocker.fetch(url, {
2029+
headers: {
2030+
[header]: method,
2031+
},
2032+
}),
2033+
new RegExp(`Header ${header} is not allowed`, "iu"),
2034+
);
2035+
});
2036+
2037+
});
2038+
});
2039+
2040+
});
19522041
});
19532042

19542043
describe("mockGlobal", () => {

0 commit comments

Comments
 (0)