From e11c9ab58b74a0242128f1efc3673f52d9b01083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sat, 18 Apr 2026 02:21:31 +0200 Subject: [PATCH] Fix /api/overpass 403 behind Caddy reverse proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The same-origin check compared the browser's Origin header against new URL(request.url), but inside the Planner container request.url is http://planner:3001/... while the browser sends Origin https://planner.trails.cool — so production always 403'd. Trust Caddy's X-Forwarded-Host and X-Forwarded-Proto headers when present (both are set by Caddy's reverse_proxy directive by default) to reconstruct the external origin the browser actually connected to. Falls back to request.url for dev and any direct (non-proxied) access. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/planner/app/routes/api.overpass.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/planner/app/routes/api.overpass.ts b/apps/planner/app/routes/api.overpass.ts index e97b7c3..571e97a 100644 --- a/apps/planner/app/routes/api.overpass.ts +++ b/apps/planner/app/routes/api.overpass.ts @@ -50,9 +50,16 @@ export async function action({ request }: Route.ActionArgs) { return new Response("Method not allowed", { status: 405 }); } + // Same-origin check. Trust Caddy's X-Forwarded-* headers when present so + // the check works behind the reverse proxy (request.url inside the container + // is http://planner:3001/..., but the browser Origin is https://planner.trails.cool). const origin = request.headers.get("origin"); const requestUrl = new URL(request.url); - const expectedOrigin = `${requestUrl.protocol}//${requestUrl.host}`; + const forwardedHost = request.headers.get("x-forwarded-host"); + const forwardedProto = request.headers.get("x-forwarded-proto"); + const host = forwardedHost ?? requestUrl.host; + const proto = forwardedProto ?? requestUrl.protocol.replace(":", ""); + const expectedOrigin = `${proto}://${host}`; if (!origin || origin !== expectedOrigin) { return new Response("Forbidden", { status: 403 }); }