From 3ccbae2471e2e712133792d19af18bb472d52041 Mon Sep 17 00:00:00 2001 From: Nicholas Ward Date: Thu, 3 Sep 2026 08:16:00 -0500 Subject: [PATCH] validate admin form origins behind proxy chain --- astro.app.config.mjs | 8 +++----- src/application/middleware.ts | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/astro.app.config.mjs b/astro.app.config.mjs index 51f2db1..c175baf 100644 --- a/astro.app.config.mjs +++ b/astro.app.config.mjs @@ -10,11 +10,9 @@ export default defineConfig({ adapter: node({ mode: "standalone" }), integrations: [preact()], security: { - checkOrigin: true, - allowedDomains: [ - { hostname: "recipes.uuard.com", protocol: "https" }, - { hostname: "recipes-admin.uuard.com", protocol: "https" }, - ], + // Origin validation is performed in application middleware because the + // edge and K3s proxy chain terminates TLS before the Node request. + checkOrigin: false, }, vite: { server: { fs: { allow: ["."] } } }, }); diff --git a/src/application/middleware.ts b/src/application/middleware.ts index 46106d4..8ff788f 100644 --- a/src/application/middleware.ts +++ b/src/application/middleware.ts @@ -2,9 +2,24 @@ import { defineMiddleware } from "astro:middleware"; import { readOnlyMode } from "../lib/runtime"; const safeMethods = new Set(["GET", "HEAD", "OPTIONS"]); +const formContentTypes = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"]; +const productionWriteOrigin = "https://recipes-admin.uuard.com"; + +function isForbiddenFormSubmission(request: Request, url: URL) { + if (safeMethods.has(request.method)) return false; + const contentType = request.headers.get("content-type")?.toLowerCase(); + if (contentType && !formContentTypes.some((value) => contentType.includes(value))) return false; + const origin = request.headers.get("origin"); + if (origin === productionWriteOrigin) return false; + const localDevelopment = url.hostname === "localhost" || url.hostname === "127.0.0.1"; + return !localDevelopment || origin !== url.origin; +} export const onRequest = defineMiddleware(async ({ request, url, redirect }, next) => { if (url.pathname === "/") return redirect("/app/", 302); + if (isForbiddenFormSubmission(request, url)) { + return new Response(`Cross-site ${request.method} form submissions are forbidden`, { status: 403 }); + } if (!readOnlyMode) return next(); if (!safeMethods.has(request.method)) { return new Response("This Formulation instance is read-only.", { status: 405, headers: { Allow: "GET, HEAD, OPTIONS" } });