diff --git a/.changeset/tricky-toes-drum.md b/.changeset/tricky-toes-drum.md new file mode 100644 index 000000000000..087e466be54d --- /dev/null +++ b/.changeset/tricky-toes-drum.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a bug where `HEAD` and `OPTIONS` requests for non-prerendered pages were incorrectly rejected with 403 FORBIDDEN diff --git a/packages/astro/src/core/app/middlewares.ts b/packages/astro/src/core/app/middlewares.ts index 7c589f0c4dce..91aefc2783e4 100644 --- a/packages/astro/src/core/app/middlewares.ts +++ b/packages/astro/src/core/app/middlewares.ts @@ -13,6 +13,9 @@ const FORM_CONTENT_TYPES = [ 'text/plain', ]; +// Note: TRACE is unsupported by undici/Node.js +const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']; + /** * Returns a middleware function in charge to check the `origin` header. * @@ -25,26 +28,22 @@ export function createOriginCheckMiddleware(): MiddlewareHandler { if (isPrerendered) { return next(); } - if (request.method === 'GET') { + // Safe methods don't require origin check + if (SAFE_METHODS.includes(request.method)) { return next(); } - const sameOrigin = - (request.method === 'POST' || - request.method === 'PUT' || - request.method === 'PATCH' || - request.method === 'DELETE') && - request.headers.get('origin') === url.origin; + const isSameOrigin = request.headers.get('origin') === url.origin; const hasContentType = request.headers.has('content-type'); if (hasContentType) { const formLikeHeader = hasFormLikeHeader(request.headers.get('content-type')); - if (formLikeHeader && !sameOrigin) { + if (formLikeHeader && !isSameOrigin) { return new Response(`Cross-site ${request.method} form submissions are forbidden`, { status: 403, }); } } else { - if (!sameOrigin) { + if (!isSameOrigin) { return new Response(`Cross-site ${request.method} form submissions are forbidden`, { status: 403, }); diff --git a/packages/astro/test/csrf-protection.test.js b/packages/astro/test/csrf-protection.test.js index 5b70e36505f6..717cc3081f81 100644 --- a/packages/astro/test/csrf-protection.test.js +++ b/packages/astro/test/csrf-protection.test.js @@ -176,6 +176,57 @@ describe('CSRF origin check', () => { }); }); + it("return a 200 when the origin doesn't match but calling HEAD", async () => { + let request; + let response; + request = new Request('http://example.com/api/', { + headers: { origin: 'http://loreum.com', 'content-type': 'multipart/form-data' }, + method: 'HEAD', + }); + response = await app.render(request); + assert.equal(response.status, 200); + + request = new Request('http://example.com/api/', { + headers: { origin: 'http://loreum.com', 'content-type': 'application/x-www-form-urlencoded' }, + method: 'HEAD', + }); + response = await app.render(request); + assert.equal(response.status, 200); + + request = new Request('http://example.com/api/', { + headers: { origin: 'http://loreum.com', 'content-type': 'text/plain' }, + method: 'HEAD', + }); + response = await app.render(request); + assert.equal(response.status, 200); + }); + + it("return a 200 when the origin doesn't match but calling OPTIONS", async () => { + let request; + let response; + request = new Request('http://example.com/api/', { + headers: { origin: 'http://loreum.com', 'content-type': 'multipart/form-data' }, + method: 'OPTIONS', + }); + response = await app.render(request); + assert.equal(response.status, 200); + + request = new Request('http://example.com/api/', { + headers: { origin: 'http://loreum.com', 'content-type': 'application/x-www-form-urlencoded' }, + method: 'OPTIONS', + }); + response = await app.render(request); + assert.equal(response.status, 200); + + request = new Request('http://example.com/api/', { + headers: { origin: 'http://loreum.com', 'content-type': 'text/plain' }, + method: 'OPTIONS', + }); + response = await app.render(request); + assert.equal(response.status, 200); + }); + + it('return 200 when calling POST/PUT/DELETE/PATCH with the correct origin', async () => { let request; let response; diff --git a/packages/astro/test/fixtures/csrf-check-origin/src/pages/api.ts b/packages/astro/test/fixtures/csrf-check-origin/src/pages/api.ts index 8aa35cc25859..cffd2c2385ba 100644 --- a/packages/astro/test/fixtures/csrf-check-origin/src/pages/api.ts +++ b/packages/astro/test/fixtures/csrf-check-origin/src/pages/api.ts @@ -27,3 +27,15 @@ export const PATCH = () => { something: 'true', }); }; + +export const HEAD = () => { + return Response.json({ + something: 'true', + }); +}; + +export const OPTIONS = () => { + return Response.json({ + something: 'true', + }); +};