summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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',
+ });
+};