diff --git a/packages/astro/src/core/app/middlewares.ts b/packages/astro/src/core/app/middlewares.ts index 1c7d6cde04..6a804627d7 100644 --- a/packages/astro/src/core/app/middlewares.ts +++ b/packages/astro/src/core/app/middlewares.ts @@ -25,22 +25,43 @@ export function createOriginCheckMiddleware(): MiddlewareHandler { if (isPrerendered) { return next(); } - const contentType = request.headers.get('content-type'); - if (contentType) { - if (FORM_CONTENT_TYPES.includes(contentType.toLowerCase())) { - const forbidden = - (request.method === 'POST' || - request.method === 'PUT' || - request.method === 'PATCH' || - request.method === 'DELETE') && - request.headers.get('origin') !== url.origin; - if (forbidden) { - return new Response(`Cross-site ${request.method} form submissions are forbidden`, { - status: 403, - }); - } + if (request.method === "GET") { + return next(); + } + const sameOrigin = + (request.method === 'POST' || + request.method === 'PUT' || + request.method === 'PATCH' || + request.method === 'DELETE') && + 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) { + return new Response(`Cross-site ${request.method} form submissions are forbidden`, { + status: 403, + }); + } + } else { + if (!sameOrigin) { + return new Response(`Cross-site ${request.method} form submissions are forbidden`, { + status: 403, + }); } } - return next(); + + return next() }); } + +function hasFormLikeHeader(contentType: string | null): boolean { + if (contentType) { + for (const FORM_CONTENT_TYPE of FORM_CONTENT_TYPES) { + if (contentType.toLowerCase().includes(FORM_CONTENT_TYPE)) { + return true; + } + } + } + return false; +} diff --git a/packages/astro/test/csrf-protection.test.js b/packages/astro/test/csrf-protection.test.js index 25aa9d0590..f8067000d2 100644 --- a/packages/astro/test/csrf-protection.test.js +++ b/packages/astro/test/csrf-protection.test.js @@ -46,6 +46,22 @@ describe('CSRF origin check', () => { }); response = await app.render(request); assert.equal(response.status, 403); + + request = new Request('http://example.com/api/', { + headers: { origin: 'http://loreum.com', 'content-type': 'application/x-www-form-urlencoded; some-other-value' }, + method: 'POST', + }); + response = await app.render(request); + assert.equal(response.status, 403); + + request = new Request('http://example.com/api/', { + headers: { origin: 'http://loreum.com', }, + method: 'POST', + credentials: 'include', + body: new Blob(["a=b"],{}) + }); + response = await app.render(request); + assert.equal(response.status, 403); }); it("return 403 when the origin doesn't match and calling a PUT", async () => {