From 860412582cac9dce7bab88aee259920a0da6eb0e Mon Sep 17 00:00:00 2001 From: WISDOM Date: Thu, 25 Jun 2026 12:53:15 +0000 Subject: [PATCH] feat(security): add CSP report-only headers and SRI for docs assets --- next.config.mjs | 10 +---- src/__tests__/csp.test.ts | 22 ++++++++++ src/__tests__/middleware.test.ts | 26 ++++++++++++ src/app/api/docs/route.ts | 16 ++++--- src/middleware.ts | 71 +++++++++++++++++--------------- 5 files changed, 98 insertions(+), 47 deletions(-) create mode 100644 src/__tests__/csp.test.ts create mode 100644 src/__tests__/middleware.test.ts diff --git a/next.config.mjs b/next.config.mjs index ee966f8..fa8908a 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -23,7 +23,6 @@ const cspHeader = [ ].join("; "); const securityHeaders = [ - // Report-Only first — switch to Content-Security-Policy once violations are reviewed { key: "Content-Security-Policy-Report-Only", value: cspHeader }, { key: "X-Content-Type-Options", value: "nosniff" }, { key: "X-Frame-Options", value: "DENY" }, @@ -45,6 +44,7 @@ const nextConfig = { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload", }, + ...securityHeaders, ], }, ]; @@ -61,14 +61,6 @@ const nextConfig = { ] : []; }, - async headers() { - return [ - { - source: "/(.*)", - headers: securityHeaders, - }, - ]; - }, }; export default withSentryConfig(nextConfig, { diff --git a/src/__tests__/csp.test.ts b/src/__tests__/csp.test.ts new file mode 100644 index 0000000..c844a4b --- /dev/null +++ b/src/__tests__/csp.test.ts @@ -0,0 +1,22 @@ +describe("CSP header builder", () => { + it("builds a nonce-based report-only policy", () => { + const nonce = "abc123"; + const policy = [ + "default-src 'self'", + `script-src 'self' 'nonce-${nonce}'`, + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https:", + "font-src 'self' https://unpkg.com", + "connect-src 'self' https://horizon.stellar.org https://horizon-testnet.stellar.org https://api.paystack.co https://api.ng.termii.com https://*.ingest.sentry.io", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + "upgrade-insecure-requests", + ].join("; "); + + expect(policy).toContain("default-src 'self'"); + expect(policy).toContain(`script-src 'self' 'nonce-${nonce}'`); + expect(policy).toContain("img-src 'self' data: https:"); + }); +}); diff --git a/src/__tests__/middleware.test.ts b/src/__tests__/middleware.test.ts new file mode 100644 index 0000000..edb3929 --- /dev/null +++ b/src/__tests__/middleware.test.ts @@ -0,0 +1,26 @@ +import { middleware } from "@/middleware"; + +describe("middleware CSP headers", () => { + it("adds CSP report-only headers to API responses", () => { + const response = middleware({ + headers: new Headers(), + nextUrl: { pathname: "/api/auth/logout" }, + method: "GET", + } as any); + + expect(response.headers.get("Content-Security-Policy-Report-Only")).toContain("default-src 'self'"); + expect(response.headers.get("Content-Security-Policy-Report-Only")).toContain("script-src"); + }); + + it("adds a nonce-based CSP header for page requests", () => { + const response = middleware({ + headers: new Headers(), + nextUrl: { pathname: "/dashboard" }, + method: "GET", + } as any); + const csp = response.headers.get("Content-Security-Policy-Report-Only") ?? ""; + + expect(csp).toContain("default-src 'self'"); + expect(csp).toMatch(/script-src 'self' 'nonce-[^']+'/); + }); +}); diff --git a/src/app/api/docs/route.ts b/src/app/api/docs/route.ts index ce965d1..96a1cf1 100644 --- a/src/app/api/docs/route.ts +++ b/src/app/api/docs/route.ts @@ -1,19 +1,26 @@ +import { headers } from "next/headers"; import { NextResponse } from "next/server"; const SPEC_URL = "/api/docs/spec"; +const SWAGGER_CSS_URL = "https://unpkg.com/swagger-ui-dist@5/swagger-ui.css"; +const SWAGGER_JS_URL = "https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"; +const SWAGGER_CSS_SHA = "sha384-9Q2fpS+xeS4ffJy6CagnwoUl+4ldAYhOs9pgZuEKxypVModhmZFzeMlvVsAjf7uT"; +const SWAGGER_JS_SHA = "sha384-IKpAWwsTL0pcw7/Amtnt2eXF4P1BK64WNuY2E/RG15SWLUW5HXzFuyqCSAr/DP8C"; -const html = ` +export async function GET() { + const nonce = headers().get("x-nonce") ?? ""; + const html = ` Ajosave API Docs - +
- - +