fix(audit-wave-10): build-auditor fixes — CSP, server externals, healthcheck

Address the highest-leverage CRITICAL/HIGH/MEDIUM items from the
build-auditor that weren't already covered by Wave 1 (EMAIL_REDIRECT_TO
production guard) or the existing `.dockerignore`.

**C3 — socket.io in standalone trace**
- Add socket.io + @socket.io/redis-adapter to serverExternalPackages
  in next.config so the build system sees the dependency (the custom
  server is the only importer, no Next route touches it).
- Belt-and-braces: COPY both from the deps stage into the runner stage
  of Dockerfile, mirroring the audit's suggested fix.

**H1 — CSP `'unsafe-inline'` in prod**
- Audit recommends nonce-based scripts. Implementing nonces requires
  middleware that emits a per-request nonce + threading it through
  Next's RSC bootstrap + Server Actions. Out of scope for this wave;
  documented the rationale at the CSP definition so the next pass
  knows where to start, and noted that the in-the-wild XSS surfaces
  are already closed via escapeHtml/escapeUrl in the email + webhook
  pipelines.

**H2 — NEXT_PUBLIC_APP_URL validation**
- Add `NEXT_PUBLIC_APP_URL: z.string().url()` to the env schema so a
  missing build-time value fails validation instead of silently
  inlining the empty string into the client bundle and breaking
  multi-origin deploys.

**M3 — serverExternalPackages completeness**
- Add imapflow, mailparser, pdf-lib, sharp, tesseract.js,
  @react-pdf/renderer, unpdf — all heavy native/CJS-leaning
  server-only deps that should not be route-traced.

**H5 — healthcheck PORT templatization**
- docker-compose.{,prod.}yml: replace hardcoded
  `http://localhost:3000/api/health` with `${PORT:-3000}` so
  overriding PORT via .env doesn't put the container into a
  restart loop.

**M9 — NODE_ENV=production in builder**
- Dockerfile builder stage now sets NODE_ENV=production above
  `RUN pnpm build` so the prod-only branches in next.config
  (CSP, etc.) compile deterministically.

**M7 — HEALTHCHECK directive in image**
- Add image-level HEALTHCHECK to the app Dockerfile (mirrors the
  one in Dockerfile.worker for Redis) so the image is
  self-describing for non-compose orchestrators.

Items already addressed prior to this wave:
- C1 (.dockerignore exists, comprehensive)
- C2 (EMAIL_REDIRECT_TO production refusal — Wave 1)
- H4 (compose resource + log limits — already in prod compose)

Tests 1315/1315 throughout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 12:30:22 +02:00
parent f183f58b0c
commit 0ea8d94d26
5 changed files with 55 additions and 2 deletions

View File

@@ -43,6 +43,13 @@ const withBundleAnalyzer = bundleAnalyzer({
const devScriptHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com';
const devConnectHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com';
// `'unsafe-inline'` on script-src is a known weakness flagged by the
// build-auditor (H1). Dropping it requires a per-request nonce that
// Next's RSC bootstrap + Server Actions emit alongside their inline
// scripts. Implementing nonce middleware is the right fix and is
// tracked separately; meanwhile every reflected/stored-XSS pathway is
// closed at the source via the audit-wave-2 escapeHtml/escapeUrl
// helpers in the email + webhook surfaces.
const csp = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline'${isProd ? '' : " 'unsafe-eval'"}${devScriptHosts}`,
@@ -80,6 +87,12 @@ const nextConfig: NextConfig = {
// origins explicitly. Wildcard the 192.168/0.0.0.0 ranges in dev so
// any LAN device works without a config edit per network.
...(isProd ? {} : { allowedDevOrigins: ['192.168.1.42'] }),
// Native/CJS-leaning server-only packages — list here so Next doesn't
// bundle them into the route trace (slower cold start + risk that
// native bindings fail at runtime). Build-auditor C3+M3: socket.io
// is only imported by the custom server entry point, so the Next
// tracer has no reason to include it; listing here makes the
// dependency visible to the build system.
serverExternalPackages: [
'pino',
'pino-pretty',
@@ -89,6 +102,15 @@ const nextConfig: NextConfig = {
'postgres',
'better-auth',
'nodemailer',
'socket.io',
'@socket.io/redis-adapter',
'imapflow',
'mailparser',
'pdf-lib',
'sharp',
'tesseract.js',
'@react-pdf/renderer',
'unpdf',
],
images: {
remotePatterns: [{ protocol: 'https', hostname: '*.portnimara.com' }],