From 66949499322cd63074fc74e3284bd5205b17c1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=A3=EF=B8=8F=20Mr=2E=20The=20Plague=20=E2=98=A3?= =?UTF-8?q?=EF=B8=8F?= <3779295+DotNetRussell@users.noreply.github.com> Date: Sun, 12 Jul 2026 11:46:19 -0400 Subject: [PATCH 1/4] Use local flag header art and strict nonce-based CSP. --- frontend/next.config.ts | 2 +- frontend/src/proxy.ts | 57 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 frontend/src/proxy.ts diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 753948c..405fca8 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -2,7 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", - // Prefer Caddy for full security headers in prod; hide framework fingerprint here too. + // CSP nonces are applied in src/proxy.ts. Caddy sets HSTS/COOP/CORP/COEP. poweredByHeader: false, async redirects() { return [ diff --git a/frontend/src/proxy.ts b/frontend/src/proxy.ts new file mode 100644 index 0000000..5e6aeaa --- /dev/null +++ b/frontend/src/proxy.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** + * Strict CSP with per-request nonces (Next.js 16 proxy convention). + * Avoids script-src 'unsafe-inline' / 'unsafe-eval' in production. + * React style={{...}} props use style-src-attr (CSP3); style tags use nonces. + */ +export function proxy(request: NextRequest) { + const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); + const isDev = process.env.NODE_ENV === "development"; + + const csp = [ + "default-src 'self'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + "object-src 'none'", + // Local assets + congress.gov member photos + "img-src 'self' data: blob: https://www.congress.gov", + "font-src 'self' data:", + // Next applies nonces to its style tags; React style attributes need style-src-attr + `style-src 'self' 'nonce-${nonce}'`, + "style-src-attr 'unsafe-inline'", + // Nonce + strict-dynamic: no 'unsafe-inline'. Dev-only eval for React refresh. + `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ""}`, + "connect-src 'self'", + "upgrade-insecure-requests", + ] + .join("; ") + .replace(/\s{2,}/g, " ") + .trim(); + + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-nonce", nonce); + // Next.js reads CSP from the request to auto-apply nonces to framework scripts + requestHeaders.set("Content-Security-Policy", csp); + + const response = NextResponse.next({ + request: { headers: requestHeaders }, + }); + response.headers.set("Content-Security-Policy", csp); + + return response; +} + +export const config = { + matcher: [ + { + source: + "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|jfif|ico)$).*)", + missing: [ + { type: "header", key: "next-router-prefetch" }, + { type: "header", key: "purpose", value: "prefetch" }, + ], + }, + ], +}; From 377d45dea3731d654aabcf388d680c5efa864aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=A3=EF=B8=8F=20Mr=2E=20The=20Plague=20=E2=98=A3?= =?UTF-8?q?=EF=B8=8F?= <3779295+DotNetRussell@users.noreply.github.com> Date: Sun, 12 Jul 2026 11:47:58 -0400 Subject: [PATCH 2/4] Add local flag CSS, layout nonce, Caddy isolation headers, docs --- frontend/src/app/globals.css | 8 +++----- frontend/src/app/layout.tsx | 13 ++++++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 7c1ffa2..4b8642d 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -89,8 +89,7 @@ body { padding: 0.5rem 0 1.25rem; background: linear-gradient(rgba(30, 64, 175, 0.45), rgba(30, 64, 175, 0.55)), - url("https://t4.ftcdn.net/jpg/08/24/57/41/360_F_824574155_qai1qN0Wxhdb6owwcolxExe8aLjiIiat.jpg") - center / cover no-repeat; + url("/images/american_flag.jpg") center / cover no-repeat; color: white; display: flex; align-items: center; @@ -102,8 +101,7 @@ body { .dark .hero-header { background: linear-gradient(rgba(11, 18, 32, 0.82), rgba(11, 18, 32, 0.92)), - url("https://t4.ftcdn.net/jpg/08/24/57/41/360_F_824574155_qai1qN0Wxhdb6owwcolxExe8aLjiIiat.jpg") - center / cover no-repeat; + url("/images/american_flag.jpg") center / cover no-repeat; } .grade-circle-a { background: #22c55e; } @@ -133,4 +131,4 @@ body { .dark footer { background-color: #1e3a8a; -} \ No newline at end of file +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 6728f12..e175099 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Suspense } from "react"; import { Inter, Roboto_Mono } from "next/font/google"; +import { headers } from "next/headers"; import { Footer } from "@/components/Footer"; import { Header } from "@/components/Header"; import { NcmecReportBanner } from "@/components/NcmecReportBanner"; @@ -57,11 +58,14 @@ export const metadata: Metadata = { }, }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + // Nonce from proxy.ts CSP; also forces dynamic rendering for per-request nonces + const nonce = (await headers()).get("x-nonce") ?? undefined; + return ( -