From db0bb015c1614786c42e4c5a358c312ae80e9a32 Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Mon, 26 Aug 2024 18:14:21 +0900 Subject: [PATCH 1/9] fix(cors)!: doesn't return `access-control-allow-origin` header when dynamic --- src/utils/internal/cors.ts | 14 +++++----- test/unit/cors.test.ts | 54 ++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/utils/internal/cors.ts b/src/utils/internal/cors.ts index 4648e440..e47e26a1 100644 --- a/src/utils/internal/cors.ts +++ b/src/utils/internal/cors.ts @@ -79,17 +79,19 @@ export function createOriginHeaders( const { origin: originOption } = options; const origin = event.request.headers.get("origin"); - if (!origin || !originOption || originOption === "*") { + if (!originOption || originOption === "*") { return { "access-control-allow-origin": "*" }; } - if (typeof originOption === "string") { - return { "access-control-allow-origin": originOption, vary: "origin" }; + if (originOption === "null") { + return { "access-control-allow-origin": "null", vary: "origin" }; } - return isCorsOriginAllowed(origin, options) - ? { "access-control-allow-origin": origin, vary: "origin" } - : {}; + if (origin && isCorsOriginAllowed(origin, options)) { + return { "access-control-allow-origin": origin, vary: "origin" }; + } + + return {}; } /** diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index ebe85e7e..24b493f6 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -180,33 +180,35 @@ describe("cors (unit)", () => { describe("createOriginHeaders", () => { it('returns an object whose `access-control-allow-origin` is `"*"` if `origin` option is not defined, or `"*"`', () => { - const eventMock = mockEvent("/", { + const hasOriginEventMock = mockEvent("/", { method: "OPTIONS", headers: { origin: "https://example.com", }, }); - const options1: H3CorsOptions = {}; - const options2: H3CorsOptions = { + const noOriginEventMock = mockEvent("/", { + method: "OPTIONS", + headers: {}, + }); + const defaultOptions: H3CorsOptions = {}; + const originWildcardOptions: H3CorsOptions = { origin: "*", }; - expect(createOriginHeaders(eventMock, options1)).toEqual({ + expect(createOriginHeaders(hasOriginEventMock, defaultOptions)).toEqual({ "access-control-allow-origin": "*", }); - expect(createOriginHeaders(eventMock, options2)).toEqual({ + expect( + createOriginHeaders(hasOriginEventMock, originWildcardOptions), + ).toEqual({ "access-control-allow-origin": "*", }); - }); - - it('returns an object whose `access-control-allow-origin` is `"*"` if `origin` header is not defined', () => { - const eventMock = mockEvent("/", { - method: "OPTIONS", - headers: {}, + expect(createOriginHeaders(noOriginEventMock, defaultOptions)).toEqual({ + "access-control-allow-origin": "*", }); - const options: H3CorsOptions = {}; - - expect(createOriginHeaders(eventMock, options)).toEqual({ + expect( + createOriginHeaders(noOriginEventMock, originWildcardOptions), + ).toEqual({ "access-control-allow-origin": "*", }); }); @@ -235,6 +237,12 @@ describe("cors (unit)", () => { origin: "http://example.com", }, }); + const noMatchEventMock = mockEvent("/", { + method: "OPTIONS", + headers: { + origin: "http://example.test", + }, + }); const options1: H3CorsOptions = { origin: ["http://example.com"], }; @@ -246,10 +254,12 @@ describe("cors (unit)", () => { "access-control-allow-origin": "http://example.com", vary: "origin", }); + expect(createOriginHeaders(noMatchEventMock, options1)).toEqual({}); expect(createOriginHeaders(eventMock, options2)).toEqual({ "access-control-allow-origin": "http://example.com", vary: "origin", }); + expect(createOriginHeaders(noMatchEventMock, options2)).toEqual({}); }); it("returns an empty object if `origin` option is one that is not allowed", () => { @@ -269,6 +279,22 @@ describe("cors (unit)", () => { expect(createOriginHeaders(eventMock, options1)).toEqual({}); expect(createOriginHeaders(eventMock, options2)).toEqual({}); }); + + it("returns an empty object if `origin` option is not wildcard and `origin` header is not defined", () => { + const eventMock = mockEvent("/", { + method: "OPTIONS", + headers: {}, + }); + const options1: H3CorsOptions = { + origin: ["http://example.com"], + }; + const options2: H3CorsOptions = { + origin: () => false, + }; + + expect(createOriginHeaders(eventMock, options1)).toEqual({}); + expect(createOriginHeaders(eventMock, options2)).toEqual({}); + }); }); describe("createMethodsHeaders", () => { From 9cdc4d1d7fa1cdd751ff77153667037908075bad Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Sat, 5 Oct 2024 02:25:54 +0900 Subject: [PATCH 2/9] docs(cors): add comments to origin option --- src/types/utils/cors.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/types/utils/cors.ts b/src/types/utils/cors.ts index d8aba97a..30a9c47a 100644 --- a/src/types/utils/cors.ts +++ b/src/types/utils/cors.ts @@ -1,6 +1,14 @@ import type { HTTPMethod } from ".."; export interface H3CorsOptions { + /** + * This determines the value of the "access-control-allow-origin" response header. + * The wildcard characters "*" can be used to allow all origins. + * An array of strings or regular expressions can be used with origin matching. + * And a custom function to validate the origin. It takes the origin as an argument and returns `true` if allowed. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + */ origin?: "*" | "null" | (string | RegExp)[] | ((origin: string) => boolean); methods?: "*" | HTTPMethod[]; allowHeaders?: "*" | string[]; From 607b43b52266ddbb8346a81940cc8778870b6233 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:27:02 +0000 Subject: [PATCH 3/9] chore: apply automated updates --- src/types/utils/cors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/utils/cors.ts b/src/types/utils/cors.ts index 30a9c47a..9ad1d7f1 100644 --- a/src/types/utils/cors.ts +++ b/src/types/utils/cors.ts @@ -6,7 +6,7 @@ export interface H3CorsOptions { * The wildcard characters "*" can be used to allow all origins. * An array of strings or regular expressions can be used with origin matching. * And a custom function to validate the origin. It takes the origin as an argument and returns `true` if allowed. - * + * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin */ origin?: "*" | "null" | (string | RegExp)[] | ((origin: string) => boolean); From 92031aa40517dc02deb795f04226d57f280c2d2c Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Mon, 7 Oct 2024 23:34:55 +0900 Subject: [PATCH 4/9] fix(cors): cannot return `"*"`, if credentials option is `true` https://w3c.github.io/webappsec-cors-for-developers/#use-vary --- src/utils/internal/cors.ts | 4 ++-- test/unit/cors.test.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/utils/internal/cors.ts b/src/utils/internal/cors.ts index e47e26a1..95c851b1 100644 --- a/src/utils/internal/cors.ts +++ b/src/utils/internal/cors.ts @@ -76,10 +76,10 @@ export function createOriginHeaders( event: H3Event, options: H3CorsOptions, ): H3AccessControlAllowOriginHeader { - const { origin: originOption } = options; + const { origin: originOption, credentials } = options; const origin = event.request.headers.get("origin"); - if (!originOption || originOption === "*") { + if ((!originOption || originOption === "*") && !credentials) { return { "access-control-allow-origin": "*" }; } diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index 24b493f6..565b4bbd 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -213,6 +213,24 @@ describe("cors (unit)", () => { }); }); + it('returns an object with `access-control-allow-origin` and `vary` keys if `origin` option is `"*"` and credentials is `true`', () => { + const eventMock = mockEvent("/", { + method: "OPTIONS", + headers: { + origin: "https://example.com", + }, + }); + const options: H3CorsOptions = { + origin: "*", + credentials: true, + }; + + expect(createOriginHeaders(eventMock, options)).toEqual({ + "access-control-allow-origin": "https://example.com", + vary: "origin", + }); + }); + it('returns an object with `access-control-allow-origin` and `vary` keys if `origin` option is `"null"`', () => { const eventMock = mockEvent("/", { method: "OPTIONS", From 1825940ca8f376afc87d6ead35e3e7b99aed4942 Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Thu, 10 Oct 2024 22:53:44 +0900 Subject: [PATCH 5/9] fix(cors): add cookie to vary if credentials option is true --- src/types/utils/cors.ts | 4 ++++ src/utils/internal/cors.ts | 8 ++++++-- test/unit/cors.test.ts | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/types/utils/cors.ts b/src/types/utils/cors.ts index 9ad1d7f1..7b552453 100644 --- a/src/types/utils/cors.ts +++ b/src/types/utils/cors.ts @@ -39,6 +39,10 @@ export type H3AccessControlAllowOriginHeader = | { "access-control-allow-origin": "*"; } + | { + "access-control-allow-origin": "*"; + vary: "cookie, origin"; + } | { "access-control-allow-origin": "null" | string; vary: "origin"; diff --git a/src/utils/internal/cors.ts b/src/utils/internal/cors.ts index 95c851b1..08e8a429 100644 --- a/src/utils/internal/cors.ts +++ b/src/utils/internal/cors.ts @@ -79,8 +79,12 @@ export function createOriginHeaders( const { origin: originOption, credentials } = options; const origin = event.request.headers.get("origin"); - if ((!originOption || originOption === "*") && !credentials) { - return { "access-control-allow-origin": "*" }; + if (!originOption || originOption === "*") { + if (!credentials) { + return { "access-control-allow-origin": "*" }; + } + // https://w3c.github.io/webappsec-cors-for-developers/#use-vary + return { "access-control-allow-origin": "*", vary: "cookie, origin" }; } if (originOption === "null") { diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index 565b4bbd..2919caa0 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -226,8 +226,8 @@ describe("cors (unit)", () => { }; expect(createOriginHeaders(eventMock, options)).toEqual({ - "access-control-allow-origin": "https://example.com", - vary: "origin", + "access-control-allow-origin": "*", + vary: "cookie, origin", }); }); From b842aeba7301c54225e705e863daec8372001381 Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Thu, 10 Oct 2024 23:00:47 +0900 Subject: [PATCH 6/9] docs(cors): update comments --- src/types/utils/cors.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/utils/cors.ts b/src/types/utils/cors.ts index 7b552453..7301c450 100644 --- a/src/types/utils/cors.ts +++ b/src/types/utils/cors.ts @@ -3,9 +3,9 @@ import type { HTTPMethod } from ".."; export interface H3CorsOptions { /** * This determines the value of the "access-control-allow-origin" response header. - * The wildcard characters "*" can be used to allow all origins. - * An array of strings or regular expressions can be used with origin matching. - * And a custom function to validate the origin. It takes the origin as an argument and returns `true` if allowed. + * If "*", it can be used to allow all origins. + * If an array of strings or regular expressions, it can be used with origin matching. + * If a custom function, it's used to validate the origin. It takes the origin as an argument and returns `true` if allowed. * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin */ From de522e904a82028b42b2cc95da9f1c1044cc5869 Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Fri, 11 Oct 2024 20:49:00 +0900 Subject: [PATCH 7/9] fix --- src/types/utils/cors.ts | 38 ++++++++++++++++++++++++++++++++++---- src/utils/internal/cors.ts | 8 ++------ test/unit/cors.test.ts | 18 ------------------ 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/types/utils/cors.ts b/src/types/utils/cors.ts index 7301c450..cc289bad 100644 --- a/src/types/utils/cors.ts +++ b/src/types/utils/cors.ts @@ -8,12 +8,46 @@ export interface H3CorsOptions { * If a custom function, it's used to validate the origin. It takes the origin as an argument and returns `true` if allowed. * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + * @default "*" */ origin?: "*" | "null" | (string | RegExp)[] | ((origin: string) => boolean); + /** + * This determines the value of the "access-control-allow-methods" response header of a preflight request. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods + * @default "*" + * @example ["GET", "HEAD", "PUT", "POST"] + */ methods?: "*" | HTTPMethod[]; + /** + * This determines the value of the "access-control-allow-headers" response header of a preflight request. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + * @default "*" + */ allowHeaders?: "*" | string[]; + /** + * This determines the value of the "access-control-expose-headers" response header. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers + * @default "*" + */ exposeHeaders?: "*" | string[]; + /** + * This determines the value of the "access-control-allow-credentials" response header. + * When request with credentials, the options that `origin`, `methods`, `exposeHeaders` and `allowHeaders` should not be set "*". + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + * @see https://fetch.spec.whatwg.org/#cors-protocol-and-credentials + * @default false + */ credentials?: boolean; + /** + * This determines the value of the "access-control-max-age" response header of a preflight request. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age + * @default false + */ maxAge?: string | false; preflight?: { statusCode?: number; @@ -39,10 +73,6 @@ export type H3AccessControlAllowOriginHeader = | { "access-control-allow-origin": "*"; } - | { - "access-control-allow-origin": "*"; - vary: "cookie, origin"; - } | { "access-control-allow-origin": "null" | string; vary: "origin"; diff --git a/src/utils/internal/cors.ts b/src/utils/internal/cors.ts index 08e8a429..e47e26a1 100644 --- a/src/utils/internal/cors.ts +++ b/src/utils/internal/cors.ts @@ -76,15 +76,11 @@ export function createOriginHeaders( event: H3Event, options: H3CorsOptions, ): H3AccessControlAllowOriginHeader { - const { origin: originOption, credentials } = options; + const { origin: originOption } = options; const origin = event.request.headers.get("origin"); if (!originOption || originOption === "*") { - if (!credentials) { - return { "access-control-allow-origin": "*" }; - } - // https://w3c.github.io/webappsec-cors-for-developers/#use-vary - return { "access-control-allow-origin": "*", vary: "cookie, origin" }; + return { "access-control-allow-origin": "*" }; } if (originOption === "null") { diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index 2919caa0..24b493f6 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -213,24 +213,6 @@ describe("cors (unit)", () => { }); }); - it('returns an object with `access-control-allow-origin` and `vary` keys if `origin` option is `"*"` and credentials is `true`', () => { - const eventMock = mockEvent("/", { - method: "OPTIONS", - headers: { - origin: "https://example.com", - }, - }); - const options: H3CorsOptions = { - origin: "*", - credentials: true, - }; - - expect(createOriginHeaders(eventMock, options)).toEqual({ - "access-control-allow-origin": "*", - vary: "cookie, origin", - }); - }); - it('returns an object with `access-control-allow-origin` and `vary` keys if `origin` option is `"null"`', () => { const eventMock = mockEvent("/", { method: "OPTIONS", From 4634da05e689a5eea66ad79d387200d59616ecb8 Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Wed, 30 Oct 2024 23:14:34 +0900 Subject: [PATCH 8/9] fix(cors)!: `isCorsOriginAllowed` returns `false` if the origin header is not defined. --- src/utils/internal/cors.ts | 25 ++++++++++++++----------- test/unit/cors.test.ts | 8 ++++---- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/utils/internal/cors.ts b/src/utils/internal/cors.ts index e47e26a1..03785480 100644 --- a/src/utils/internal/cors.ts +++ b/src/utils/internal/cors.ts @@ -39,23 +39,26 @@ export function resolveCorsOptions( } /** - * Check if the incoming request is a CORS request. + * Check if the origin is allowed. */ export function isCorsOriginAllowed( - origin: string | undefined, + origin: string | null | undefined, options: H3CorsOptions, ): boolean { const { origin: originOption } = options; - if ( - !origin || - !originOption || - originOption === "*" || - originOption === "null" - ) { + if (!origin) { + return false; + } + + if (!originOption || originOption === "*") { return true; } + if (typeof originOption === "function") { + return originOption(origin); + } + if (Array.isArray(originOption)) { return originOption.some((_origin) => { if (_origin instanceof RegExp) { @@ -66,7 +69,7 @@ export function isCorsOriginAllowed( }); } - return originOption(origin); + return originOption === origin; } /** @@ -87,8 +90,8 @@ export function createOriginHeaders( return { "access-control-allow-origin": "null", vary: "origin" }; } - if (origin && isCorsOriginAllowed(origin, options)) { - return { "access-control-allow-origin": origin, vary: "origin" }; + if (isCorsOriginAllowed(origin, options)) { + return { "access-control-allow-origin": origin!, vary: "origin" }; } return {}; diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index 24b493f6..76803302 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -106,11 +106,11 @@ describe("cors (unit)", () => { }); describe("isCorsOriginAllowed", () => { - it("returns `true` if `origin` header is not defined", () => { + it("returns `false` if `origin` header is not defined", () => { const origin = undefined; const options: H3CorsOptions = {}; - expect(isCorsOriginAllowed(origin, options)).toEqual(true); + expect(isCorsOriginAllowed(origin, options)).toEqual(false); }); it("returns `true` if `origin` option is not defined", () => { @@ -129,13 +129,13 @@ describe("cors (unit)", () => { expect(isCorsOriginAllowed(origin, options)).toEqual(true); }); - it('returns `true` if `origin` option is `"null"`', () => { + it('returns `false` if `origin` option is `"null"`', () => { const origin = "https://example.com"; const options: H3CorsOptions = { origin: "null", }; - expect(isCorsOriginAllowed(origin, options)).toEqual(true); + expect(isCorsOriginAllowed(origin, options)).toEqual(false); }); it("can detect allowed origin (string)", () => { From d476c7288b14f6e63c7b7eb830560a32182b9d99 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:16:36 +0000 Subject: [PATCH 9/9] chore: apply automated updates --- docs/2.utils/98.advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/2.utils/98.advanced.md b/docs/2.utils/98.advanced.md index a3552869..a65aa01a 100644 --- a/docs/2.utils/98.advanced.md +++ b/docs/2.utils/98.advanced.md @@ -192,7 +192,7 @@ router.use("/", async (event) => { ### `isCorsOriginAllowed(origin, options)` -Check if the incoming request is a CORS request. +Check if the origin is allowed. ### `isPreflightRequest(event)`