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 (
-
+
@@ -82,4 +89,4 @@ export default function RootLayout({
);
-}
\ No newline at end of file
+}
From 8e58b53e66a3b3620ff108de3a8e685fdef87e60 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:48:11 -0400
Subject: [PATCH 3/4] Update Caddy templates with COOP/CORP/COEP; CSP lives in
Next proxy
---
Caddyfile.example | 17 +++++++++++------
deploy/Caddyfile.ip | 5 ++++-
2 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/Caddyfile.example b/Caddyfile.example
index 900cc09..de2b70b 100644
--- a/Caddyfile.example
+++ b/Caddyfile.example
@@ -2,10 +2,12 @@
#
# Security headers below address common baseline scan failures:
# Strict-Transport-Security — HTTPS-only (HSTS)
-# Content-Security-Policy — XSS / injection hardening
# X-Frame-Options — clickjacking protection
-# CSP is intentionally practical for Next.js (inline theme bootstrap + styles).
-# Tighten script-src (nonces/hashes) later if you outgrow 'unsafe-inline'.
+# COOP / CORP / COEP — cross-origin isolation (defense-in-depth)
+#
+# Content-Security-Policy is set by the Next.js proxy (frontend/src/proxy.ts)
+# with per-request nonces — do NOT re-declare CSP here or scanners / browsers
+# will see a second policy that still allows unsafe-inline and breaks nonces.
operationchildshield.org, www.operationchildshield.org {
encode gzip
@@ -14,7 +16,7 @@ operationchildshield.org, www.operationchildshield.org {
# Force HTTPS for 1 year on this host and subdomains
Strict-Transport-Security "max-age=31536000; includeSubDomains"
- # Clickjacking: deny framing (also covered by CSP frame-ancestors)
+ # Clickjacking: deny framing (also covered by Next CSP frame-ancestors)
X-Frame-Options "DENY"
# MIME sniffing protection
@@ -26,8 +28,11 @@ operationchildshield.org, www.operationchildshield.org {
# Disable unused powerful browser features
Permissions-Policy "camera=(), microphone=(), geolocation=()"
- # Baseline CSP for Next.js App Router + congress.gov member photos
- Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; img-src 'self' data: blob: https://www.congress.gov; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' https://operationchildshield.org https://www.operationchildshield.org; upgrade-insecure-requests"
+ # Cross-origin isolation (defense-in-depth)
+ # COEP credentialless allows third-party images (e.g. congress.gov) without CORP
+ Cross-Origin-Opener-Policy "same-origin"
+ Cross-Origin-Resource-Policy "same-origin"
+ Cross-Origin-Embedder-Policy "credentialless"
# Reduce server fingerprinting (Next.js / uvicorn / Caddy defaults)
-Server
diff --git a/deploy/Caddyfile.ip b/deploy/Caddyfile.ip
index 1bb35fd..9fc383b 100644
--- a/deploy/Caddyfile.ip
+++ b/deploy/Caddyfile.ip
@@ -2,6 +2,7 @@
# Replace :80 with your domain once DNS A records are ready (see Caddyfile.example).
#
# HSTS is intentionally omitted on plain HTTP.
+# CSP is set by the Next.js proxy (frontend/src/proxy.ts) with nonces.
:80 {
encode gzip
@@ -11,7 +12,9 @@
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
- Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; img-src 'self' data: blob: https://www.congress.gov; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'"
+ Cross-Origin-Opener-Policy "same-origin"
+ Cross-Origin-Resource-Policy "same-origin"
+ Cross-Origin-Embedder-Policy "credentialless"
-Server
-X-Powered-By
}
From 4f87e7690e172c6016b24a8a863962178c8c0c3e 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:48:26 -0400
Subject: [PATCH 4/4] Document nonce CSP and isolation headers in deploy guide
---
docs/DEPLOY-GITHUB-DO.md | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/docs/DEPLOY-GITHUB-DO.md b/docs/DEPLOY-GITHUB-DO.md
index f631b72..7710623 100644
--- a/docs/DEPLOY-GITHUB-DO.md
+++ b/docs/DEPLOY-GITHUB-DO.md
@@ -83,24 +83,28 @@ Production `Caddyfile.example` sets baseline browser security headers on both ap
| Header | Purpose |
|--------|---------|
| `Strict-Transport-Security` | HSTS — force HTTPS for 1 year (+ subdomains) |
-| `Content-Security-Policy` | Limit script/style/img/connect sources (Next.js-friendly) |
| `X-Frame-Options` | Deny framing (clickjacking) |
| `X-Content-Type-Options` | `nosniff` |
| `Referrer-Policy` / `Permissions-Policy` | Extra hardening |
+| `Cross-Origin-Opener-Policy` | `same-origin` — isolate top-level browsing context |
+| `Cross-Origin-Resource-Policy` | `same-origin` — block cross-site embedding of our resources |
+| `Cross-Origin-Embedder-Policy` | `credentialless` — isolation without breaking congress.gov images |
+
+**Content-Security-Policy** is set by the Next.js app (`frontend/src/proxy.ts`) with **per-request nonces** — no `script-src 'unsafe-inline'` / `'unsafe-eval'` in production. Do **not** re-declare CSP in Caddy (a second policy would defeat nonces and reintroduce scanner findings).
Also:
- Strips `Server` and `X-Powered-By` from responses
- Overwrites `X-Real-IP` / `X-Forwarded-For` with the real peer (blocks client XFF spoofing used against app rate limits)
-After updating the server `Caddyfile`, reload Caddy:
+After updating the server `Caddyfile`, reload Caddy (and redeploy the frontend image so proxy CSP is live):
```bash
ssh -i ~/.ssh/YOUR_DEPLOY_KEY USER@HOST
cd /opt/operationchildshield
docker compose -f docker-compose.prod.yml exec caddy caddy reload --config /etc/caddy/Caddyfile
# verify:
-curl -sI https://operationchildshield.org/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type'
+curl -sI https://operationchildshield.org/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|cross-origin'
```
## Backend security notes