summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Cornelius Roemer <cornelius.roemer@gmail.com> 2025-02-13 13:05:27 +0100
committerGravatar GitHub <noreply@github.com> 2025-02-13 12:05:27 +0000
commit2ed67d5dc5c8056f9ab1e29e539bf086b93c60c2 (patch)
treefb133d5723234f6c8434f4db361e031f1152afe8
parentf392bef0cf4f8ad36eda97864043acb756143316 (diff)
downloadastro-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.md5
-rw-r--r--packages/astro/src/core/app/middlewares.ts17
-rw-r--r--packages/astro/test/csrf-protection.test.js51
-rw-r--r--packages/astro/test/fixtures/csrf-check-origin/src/pages/api.ts12
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',
+ });
+};