Skip to content

Commit

Permalink
fix: respond with 200 to HEAD requests for non-prerendered pages as w…
Browse files Browse the repository at this point in the history
…ell (#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 <[email protected]>
  • Loading branch information
corneliusroemer and ematipico authored Feb 13, 2025
1 parent f392bef commit 2ed67d5
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-toes-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes a bug where `HEAD` and `OPTIONS` requests for non-prerendered pages were incorrectly rejected with 403 FORBIDDEN
17 changes: 8 additions & 9 deletions packages/astro/src/core/app/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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,
});
Expand Down
51 changes: 51 additions & 0 deletions packages/astro/test/csrf-protection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions packages/astro/test/fixtures/csrf-check-origin/src/pages/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
};

0 comments on commit 2ed67d5

Please sign in to comment.