From b4fac95f37da3985dbc4fbf64d04dd509ec86c2c Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Tue, 2 Jun 2026 13:25:12 -0500 Subject: [PATCH] fix(web): enforce upstream allowlist in vite dev proxy Mirrors the nginx allowlist (spec section D) so make dev-web matches prod: when OPENCONCHO_UPSTREAM_ALLOWLIST is set, non-matching upstreams get 403 + X-Honcho-Proxy-Reject; unset stays open. --- packages/web/vite.config.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index 1da63c8..2fed9ed 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -18,6 +18,19 @@ const { version } = JSON.parse( // so req.url is already the upstream path (e.g. /v3/workspaces/list). function honchoApiProxy(): Plugin { const HEADER = "x-honcho-upstream"; + // Mirror nginx's allowlist (spec §D): unset/empty => open; otherwise only + // matching upstream hosts forward. Glob `*` -> any non-slash run, like nginx. + const raw = process.env.OPENCONCHO_UPSTREAM_ALLOWLIST?.trim(); + const allowlist: RegExp[] | null = raw + ? raw + .split(",") + .map((h) => h.trim()) + .filter(Boolean) + .map((host) => { + const esc = host.replace(/[.]/g, "\\.").replace(/[*]/g, "[^/]*"); + return new RegExp(`^https?://${esc}(:[0-9]+)?(/.*)?$`); + }) + : null; return { name: "honcho-api-proxy", configureServer(server) { @@ -29,6 +42,12 @@ function honchoApiProxy(): Plugin { res.end(); return; } + if (allowlist && !allowlist.some((re) => re.test(upstream))) { + res.statusCode = 403; + res.setHeader("X-Honcho-Proxy-Reject", "allowlist"); + res.end(); + return; + } const target = upstream.replace(/\/+$/, "") + (req.url ?? ""); const chunks: Buffer[] = []; for await (const c of req) chunks.push(c as Buffer);