Skip to content

Commit 0e529ee

Browse files
authored
fix: Ensure Access-Control-Allow-Origin header contains valid origin (#68)
* fix: Ensure Access-Control-Allow-Origin header contains valid origin * Fix FetchMocker * Fix Origin detection
1 parent dc78d00 commit 0e529ee

File tree

4 files changed

+145
-13
lines changed

4 files changed

+145
-13
lines changed

src/cors.js

+44-6
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,20 @@ function isCorsSafeListedRequestHeader(name, value) {
245245

246246
}
247247

248+
/**
249+
* Checks if a string is a valid origin.
250+
* @param {string} origin The origin to validate.
251+
* @returns {boolean} `true` if the origin is valid, `false` otherwise.
252+
*/
253+
function isValidOrigin(origin) {
254+
try {
255+
new URL(origin);
256+
return true;
257+
} catch {
258+
return false;
259+
}
260+
}
261+
248262
//-----------------------------------------------------------------------------
249263
// Exports
250264
//-----------------------------------------------------------------------------
@@ -298,27 +312,51 @@ export class CorsPreflightError extends CorsError {
298312
* Asserts that the response has the correct CORS headers.
299313
* @param {Response} response The response to check.
300314
* @param {string} origin The origin to check against.
315+
* @param {boolean} isPreflight `true` if this is a preflight request, `false` otherwise.
301316
* @returns {void}
302317
* @throws {Error} When the response doesn't have the correct CORS headers.
303318
*/
304-
export function assertCorsResponse(response, origin) {
319+
export function assertCorsResponse(response, origin, isPreflight = false) {
305320
const originHeader = response.headers.get(CORS_ALLOW_ORIGIN);
321+
const NetworkError = isPreflight ? CorsPreflightError : CorsError;
306322

307323
if (!originHeader) {
308-
throw new CorsPreflightError(
324+
throw new NetworkError(
309325
response.url,
310326
origin,
311327
"No 'Access-Control-Allow-Origin' header is present on the requested resource.",
312328
);
313329
}
314-
315-
if (originHeader !== "*" && originHeader !== origin) {
316-
throw new CorsError(
330+
331+
// multiple values are not allowed
332+
if (originHeader.includes(",")) {
333+
throw new NetworkError(
317334
response.url,
318335
origin,
319-
`The 'Access-Control-Allow-Origin' header has a value '${originHeader}' that is not equal to the supplied origin.`,
336+
`The 'Access-Control-Allow-Origin' header contains multiple values '${originHeader}', but only one is allowed.`,
320337
);
321338
}
339+
340+
if (originHeader !== "*") {
341+
// must be a valid origin
342+
if (!isValidOrigin(origin)) {
343+
throw new NetworkError(
344+
response.url,
345+
origin,
346+
`The 'Access-Control-Allow-Origin' header contains the invalid value '${originHeader}'.`,
347+
);
348+
}
349+
350+
const originUrl = new URL(origin);
351+
352+
if (originUrl.origin !== originHeader) {
353+
throw new NetworkError(
354+
response.url,
355+
origin,
356+
`The 'Access-Control-Allow-Origin' header has a value '${originHeader}' that is not equal to the supplied origin.`,
357+
);
358+
}
359+
}
322360
}
323361

324362
/**

src/fetch-mocker.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ export class FetchMocker {
461461
);
462462
}
463463

464-
assertCorsResponse(preflightResponse, this.#baseUrl.origin);
464+
assertCorsResponse(preflightResponse, this.#baseUrl.origin, true);
465465

466466
// create the preflight data
467467
preflightData = new CorsPreflightData(preflightResponse.headers);

tests/cors.test.js

+96-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
* @fileoverview Tests for the CORS utilities.
33
* @autor Nicholas C. Zakas
44
*/
5-
/* global Request, Headers */
5+
/* global Request, Headers, Response */
66

77
//-----------------------------------------------------------------------------
88
// Imports
99
//-----------------------------------------------------------------------------
1010

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

1414
//-----------------------------------------------------------------------------
1515
// Tests
@@ -251,4 +251,98 @@ describe("http", () => {
251251
);
252252
});
253253
});
254+
255+
describe("assertCorsResponse()", () => {
256+
it("should not throw when Access-Control-Allow-Origin is *", () => {
257+
const headers = new Headers({
258+
"Access-Control-Allow-Origin": "*"
259+
});
260+
const response = new Response(null, { headers });
261+
262+
assert.doesNotThrow(() => {
263+
assertCorsResponse(response, "https://example.com");
264+
});
265+
});
266+
267+
it("should not throw when Access-Control-Allow-Origin matches origin", () => {
268+
const origin = "https://example.com";
269+
const headers = new Headers({
270+
"Access-Control-Allow-Origin": origin
271+
});
272+
const response = new Response(null, { headers });
273+
274+
assert.doesNotThrow(() => {
275+
assertCorsResponse(response, origin);
276+
});
277+
});
278+
279+
it("should throw when Access-Control-Allow-Origin header is missing", () => {
280+
const response = new Response();
281+
const origin = "https://example.com";
282+
283+
assert.throws(() => {
284+
assertCorsResponse(response, origin);
285+
}, {
286+
name: "CorsError",
287+
message: `Access to fetch at '${response.url}' from origin '${origin}' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.`
288+
});
289+
});
290+
291+
it("should throw when Access-Control-Allow-Origin doesn't match origin", () => {
292+
const headers = new Headers({
293+
"Access-Control-Allow-Origin": "https://example.com"
294+
});
295+
const response = new Response(null, { headers });
296+
const origin = "https://other.com";
297+
298+
assert.throws(() => {
299+
assertCorsResponse(response, origin);
300+
}, {
301+
name: "CorsError",
302+
message: `Access to fetch at '${response.url}' from origin '${origin}' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'https://example.com' that is not equal to the supplied origin.`
303+
});
304+
});
305+
306+
it("should throw when Access-Control-Allow-Origin contains multiple values", () => {
307+
const headers = new Headers({
308+
"Access-Control-Allow-Origin": "https://example.com, https://other.com"
309+
});
310+
const response = new Response(null, { headers });
311+
const origin = "https://example.com";
312+
313+
assert.throws(() => {
314+
assertCorsResponse(response, origin);
315+
}, {
316+
name: "CorsError",
317+
message: `Access to fetch at '${response.url}' from origin '${origin}' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values 'https://example.com, https://other.com', but only one is allowed.`
318+
});
319+
});
320+
321+
it("should throw CorsPreflightError when Access-Control-Allow-Origin header is missing in preflight", () => {
322+
const response = new Response();
323+
const origin = "https://example.com";
324+
325+
assert.throws(() => {
326+
assertCorsResponse(response, origin, true);
327+
}, {
328+
name: "CorsPreflightError",
329+
message: `Access to fetch at '${response.url}' from origin '${origin}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.`
330+
});
331+
});
332+
333+
it("should throw CorsPreflightError when Access-Control-Allow-Origin doesn't match origin in preflight", () => {
334+
const headers = new Headers({
335+
"Access-Control-Allow-Origin": "https://example.com"
336+
});
337+
const response = new Response(null, { headers });
338+
const origin = "https://other.com";
339+
340+
assert.throws(() => {
341+
assertCorsResponse(response, origin, true);
342+
}, {
343+
name: "CorsPreflightError",
344+
message: `Access to fetch at '${response.url}' from origin '${origin}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value 'https://example.com' that is not equal to the supplied origin.`
345+
});
346+
});
347+
});
254348
});

tests/fetch-mocker.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -841,8 +841,8 @@ describe("FetchMocker", () => {
841841
server.get("/hello", 200);
842842

843843
await assert.rejects(fetchMocker.fetch(url), {
844-
name: "CorsPreflightError",
845-
message: `Access to fetch at '${url.href}' from origin '${origin}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.`,
844+
name: "CorsError",
845+
message: `Access to fetch at '${url.href}' from origin '${origin}' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.`,
846846
});
847847
});
848848

@@ -1446,8 +1446,8 @@ describe("FetchMocker", () => {
14461446
await assert.rejects(
14471447
fetchMocker.fetch(url, { headers: { Custom: "Foo" } }),
14481448
{
1449-
name: "CorsError",
1450-
message: `Access to fetch at '${url.href}' from origin '${origin}' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'https://api.example.com' that is not equal to the supplied origin.`,
1449+
name: "CorsPreflightError",
1450+
message: `Access to fetch at '${url.href}' from origin '${origin}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value 'https://api.example.com' that is not equal to the supplied origin.`,
14511451
},
14521452
);
14531453
});

0 commit comments

Comments
 (0)