From dadcbbecbe268ce0b6aefe5a3cc388c486230873 Mon Sep 17 00:00:00 2001 From: gsknnft Date: Thu, 16 Apr 2026 19:23:06 -0400 Subject: [PATCH] hardening: drop unsafe-eval in production CSP; add TRUSTED_PROXY IP resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit next.config.ts: - unsafe-eval removed from production script-src (Next.js dev/HMR needs it, but production build does not; React and Three.js make no use of eval) - connect-src intentionally kept broad with note: gateway URLs are user-configured at runtime, cannot be enumerated at build time server/access-gate.js: - Add resolveClientIp() helper: when TRUSTED_PROXY=1 env var is set, prefer the first value of X-Forwarded-For for rate-limiter keying (correct behavior behind nginx/Caddy/Vercel edge). Without the flag, remoteAddress is used (safe default for direct exposure — prevents X-Forwarded-For spoofing by untrusted clients). Authored-By: GSKNNFT --- next.config.ts | 9 ++++++++- server/access-gate.js | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/next.config.ts b/next.config.ts index cad6b4c..15b59ba 100644 --- a/next.config.ts +++ b/next.config.ts @@ -12,7 +12,14 @@ const securityHeaders = [ "img-src 'self' data: blob: http: https:", "font-src 'self' data: https:", "style-src 'self' 'unsafe-inline' https:", - "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:", + // 'unsafe-eval' is required by Next.js dev mode (source maps, HMR). + // In production it is dropped — React and Three.js do not need eval. + ...(process.env.NODE_ENV !== "production" + ? ["script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:"] + : ["script-src 'self' 'unsafe-inline' blob:"]), + // connect-src is intentionally broad: gateway URLs are user-configured + // at runtime and cannot be enumerated at build time. + // Restrict further when a fixed deployment target is known. "connect-src 'self' ws: wss: http: https:", "media-src 'self' blob: data: http: https:", "worker-src 'self' blob:", diff --git a/server/access-gate.js b/server/access-gate.js index 1f5e72e..3c75d2c 100644 --- a/server/access-gate.js +++ b/server/access-gate.js @@ -61,6 +61,24 @@ const createRateLimiter = (maxAttempts = 10, windowMs = 60_000) => { }; }; +/** + * Resolve client IP for rate limiting. + * When TRUSTED_PROXY=1 is set, the first value of X-Forwarded-For is used. + * Only set TRUSTED_PROXY=1 when this server sits behind a reverse proxy that + * you control (nginx, Caddy, Vercel edge). Without it, X-Forwarded-For is + * ignored to prevent spoofing by direct clients. + */ +const resolveClientIp = (req) => { + if (process.env.TRUSTED_PROXY === "1") { + const forwarded = req.headers?.["x-forwarded-for"]; + if (typeof forwarded === "string") { + const first = forwarded.split(",")[0]?.trim(); + if (first) return first; + } + } + return req.socket?.remoteAddress || "unknown"; +}; + function createAccessGate(options) { const token = String(options?.token ?? "").trim(); const cookieName = String(options?.cookieName ?? "studio_access").trim() || "studio_access"; @@ -70,7 +88,7 @@ function createAccessGate(options) { const getAuthState = (req) => { if (!enabled) return { authorized: true, limited: false }; - const ip = req.socket?.remoteAddress || "unknown"; + const ip = resolveClientIp(req); const cookieHeader = req.headers?.cookie; const cookies = parseCookies(cookieHeader); const authorized = safeCompare(cookies[cookieName] || "", token);