Security Scan (SIAX Cloud) / gitleaks (push) Failing after 21s
CI (SIAX Cloud) / security (push) Successful in 27s
Security Scan (SIAX Cloud) / trivy (push) Failing after 47s
CI (SIAX Cloud) / quality (push) Successful in 3m28s
Security Scan (SIAX Cloud) / license-inventory (push) Successful in 8m59s
BACKLOG P0-13: next.config.mjs now sets HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and a same-origin CSP on every route. Previously the only security header came from Vercel's platform default (HSTS) -- nothing was set by the app itself, so it would have silently disappeared on cutover to Coolify/Traefik. Verified at runtime (curl -I against a production build), not just that the build compiles. BACKLOG P0-16: Trivy in security.yml/nightly.yml now actually fails the build on HIGH/CRITICAL findings (`--exit-code 1`) instead of the previous `--exit-code 0` (purely advisory, never blocked a merge). Verified locally against this repo's real dependency tree -- 19 HIGH findings currently exist in pnpm-lock.yaml (next.js, postcss, sharp, nanoid), several in next.js itself relating to middleware/auth bypass. See PR description: this is expected to turn the security.yml `trivy` job red until those are bumped in a follow-up -- that's the fix working as intended, not a regression, but it needs a human decision on timing given the sibling auth-gate PR (#11). `services/scrapers/` stays explicitly excluded from the scan (--skip-dirs) -- that tree is a dated, deliberate keep-decision under active review in a separate thread (BACKLOG P3-3) and is out of scope here. BACKLOG P0-19: dedicated gitleaks job added to security.yml, full git history (--log-opts=--all), pinned + checksum-verified binary. Verified locally against this repo's actual history first (81 commits, no leaks) so this blocking gate doesn't open red on day one. BACKLOG P1-14: SBOM (CycloneDX) + license inventory (`pnpm licenses list`) generated as informational, non-blocking build artifacts. actions/checkout, actions/setup-node, actions/cache, actions/upload-artifact all pinned to commit SHA (was floating @v4 tags) across every workflow file, Trivy/gitleaks pinned to specific released versions instead of floating install scripts. cosign image-signing explicitly NOT added -- there is no container-registry publish step in this repo's CI to attach a signature to yet (Coolify builds directly from git); flagging as a real follow-up once that pipeline exists. BACKLOG P1-15: explicit `permissions: contents: read` + `concurrency` added to every workflow that lacked them (only .github/workflows/ci.yml already had concurrency). Pinned pnpm-store cache added to ci.yml's quality job. Root CODEOWNERS added (`* @sax3l`) so Gitea has someone to assign once required-approvals is turned on (separate repo-settings change, not a file change). The "doctor step" part of P1-15 is added in the follow-up repo- hygiene PR, alongside the `siax:doctor` script it invokes.
64 lines
2.4 KiB
JavaScript
64 lines
2.4 KiB
JavaScript
// BACKLOG P0-13 (SEC-06): the app previously shipped no security headers of
|
|
// its own -- HSTS existed only as a platform default injected by Vercel, so
|
|
// it would have silently disappeared on cutover to Coolify/Traefik (see
|
|
// docs/architecture/ARCHITECTURE.md §5/§8). These are set here, at the
|
|
// framework level, so they travel with the app regardless of host.
|
|
//
|
|
// CSP is same-origin-only (`'self'`) plus the minimum carve-outs this app
|
|
// actually needs today:
|
|
// - `'unsafe-inline'` on script-src/style-src: Next.js's App Router injects
|
|
// small inline hydration/RSC payload scripts and Tailwind can emit
|
|
// inline style attributes; this repo doesn't yet wire a per-request
|
|
// nonce through middleware+layout (a real follow-up -- see BACKLOG --
|
|
// but out of scope for this pass, which fixes "zero headers", not "zero
|
|
// inline script tolerance").
|
|
// - `form-action` allows Stripe's hosted Checkout domains: the payment
|
|
// flow (app/api/payments/[order_id]/deposit) does a full top-level
|
|
// redirect to a Stripe-hosted page, never an embedded iframe/script
|
|
// (grep-verified: no @stripe/stripe-js dependency, no client-side
|
|
// Stripe.js load anywhere in app/).
|
|
// No other third-party origins are contacted from the browser (no
|
|
// analytics, no next/font/google, no embedded widgets -- grep-verified).
|
|
const securityHeaders = [
|
|
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
|
|
{ key: "X-Frame-Options", value: "DENY" },
|
|
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), interest-cohort=()" },
|
|
{
|
|
key: "Content-Security-Policy",
|
|
value: [
|
|
"default-src 'self'",
|
|
"script-src 'self' 'unsafe-inline'",
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' data: blob:",
|
|
"font-src 'self' data:",
|
|
"connect-src 'self'",
|
|
"form-action 'self' https://checkout.stripe.com https://api.stripe.com",
|
|
"frame-ancestors 'none'",
|
|
"base-uri 'self'",
|
|
"object-src 'none'",
|
|
].join("; "),
|
|
},
|
|
]
|
|
|
|
/** @type {import('next').NextConfig} */
|
|
const nextConfig = {
|
|
typescript: {
|
|
ignoreBuildErrors: true,
|
|
},
|
|
images: {
|
|
unoptimized: true,
|
|
},
|
|
async headers() {
|
|
return [
|
|
{
|
|
source: "/(.*)",
|
|
headers: securityHeaders,
|
|
},
|
|
]
|
|
},
|
|
}
|
|
|
|
export default nextConfig
|