diff options
author | 2025-02-13 13:05:27 +0100 | |
---|---|---|
committer | 2025-02-13 12:05:27 +0000 | |
commit | 2ed67d5dc5c8056f9ab1e29e539bf086b93c60c2 (patch) | |
tree | fb133d5723234f6c8434f4db361e031f1152afe8 | |
parent | f392bef0cf4f8ad36eda97864043acb756143316 (diff) | |
download | astro-2ed67d5dc5c8056f9ab1e29e539bf086b93c60c2.tar.gz astro-2ed67d5dc5c8056f9ab1e29e539bf086b93c60c2.tar.zst astro-2ed67d5dc5c8056f9ab1e29e539bf086b93c60c2.zip |
fix: respond with 200 to HEAD requests for non-prerendered pages as well (#13101)
* fix: respond with 200 to HEAD requests for non-prerendered pages as well
Fixes #13079
Inspired by @joshmkennedy's PR #13100
* chore: add more test cases
* Update .changeset/tricky-toes-drum.md
* chore: remove trace method
---------
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
-rw-r--r-- | .changeset/tricky-toes-drum.md | 5 | ||||
-rw-r--r-- | packages/astro/src/core/app/middlewares.ts | 17 | ||||
-rw-r--r-- | packages/astro/test/csrf-protection.test.js | 51 | ||||
-rw-r--r-- | packages/astro/test/fixtures/csrf-check-origin/src/pages/api.ts | 12 |
4 files changed, 76 insertions, 9 deletions
diff --git a/.changeset/tricky-toes-drum.md b/.changeset/tricky-toes-drum.md new file mode 100644 index 000000000..087e466be --- /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 7c589f0c4..91aefc278 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 5b70e3650..717cc3081 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 8aa35cc25..cffd2c238 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', + }); +}; |