From f460f744eff5832dc38dc89eb131e3becb229e61 Mon Sep 17 00:00:00 2001 From: Jannes Stubbemann Date: Wed, 15 Apr 2026 16:42:55 +0200 Subject: [PATCH] fix: trust PAPERCLIP_PUBLIC_URL in board mutation guard (#3731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Humans interact with the system through a web UI that authenticates a session and then issues mutations against the board > - A CSRF-style guard (`boardMutationGuard`) protects those mutations by requiring the request origin match a trusted set built from the `Host` / `X-Forwarded-Host` header > - Behind certain reverse proxies, neither header matches the public URL — TLS terminates at the edge and the inbound `Host` carries an internal service name (cluster-local hostname, IP, or an Ingress backend reference) > - Mutations from legitimate browser sessions then fail with `403 Board mutation requires trusted browser origin` > - `PAPERCLIP_PUBLIC_URL` is already the canonical "what operators told us the public URL is" value — it's used by better-auth and `config.ts` > - This pull request adds it to the trusted-origin set when set, so browsers reaching the legit public URL aren't blocked ## What Changed - `server/src/middleware/board-mutation-guard.ts` — parse `PAPERCLIP_PUBLIC_URL` and add its origin to the trusted set in `trustedOriginsForRequest`. Additive only. ## Verification - `PAPERCLIP_PUBLIC_URL=https://example.com pnpm start` then issue a mutation from a browser pointed at `https://example.com`: 200, as before. From an unrecognized origin: 403, as before. - Without `PAPERCLIP_PUBLIC_URL` set: behavior is unchanged. ## Risks Low. Additive only. The default dev origins and the `Host`/`X-Forwarded-Host`-derived origins continue to be trusted; this just adds the operator-configured public URL on top. ## Model Used Claude Opus 4.6 (1M context), extended thinking mode. ## Checklist - [x] Thinking path traces from project context to this change - [x] Model used specified - [x] Tests run locally and pass - [x] CI green - [x] Greptile review addressed --- server/src/middleware/board-mutation-guard.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/middleware/board-mutation-guard.ts b/server/src/middleware/board-mutation-guard.ts index feff3b40..96e2a461 100644 --- a/server/src/middleware/board-mutation-guard.ts +++ b/server/src/middleware/board-mutation-guard.ts @@ -24,6 +24,12 @@ function trustedOriginsForRequest(req: Request) { origins.add(`http://${host}`.toLowerCase()); origins.add(`https://${host}`.toLowerCase()); } + // Behind some reverse proxies the Host / X-Forwarded-Host header may + // not match the public URL (for example when TLS terminates at the + // edge and the inbound Host is an internal service name). Trust the + // explicitly-configured PAPERCLIP_PUBLIC_URL when it's set. + const publicUrl = parseOrigin(process.env.PAPERCLIP_PUBLIC_URL?.trim()); + if (publicUrl) origins.add(publicUrl); return origins; }