Files
anouma/next.config.ts
T
maro 82c4261a81 Fix security and correctness issues found in full-codebase audit
- Restrict meetingPassword field so it isn't leaked via the public REST/GraphQL API
- Fix booking slot computation to use Europe/Berlin time instead of the server's UTC clock
- Reset email verification when a customer changes their email address
- Fix ICS calendar injection via unescaped carriage returns in event descriptions
- Bind WebRTC meeting kicks to a persistent client id so they survive a rejoin
- Move the meeting signaling token out of the WebSocket query string into the first message
- Prevent duplicate signaling connections from leaking when the same participant reconnects
- Use the rightmost X-Forwarded-For entry for rate limiting (single trusted reverse proxy)
- Use constant-time comparison for the cron endpoint's bearer token
- Remove SVG from allowed Media upload types (stored-XSS risk, unused anyway)
- Add baseline security headers and a CSP to next.config.ts
- Fix CI/CD script injection via unsanitized git ref names in release.yml
2026-09-06 21:59:39 +02:00

78 lines
3.2 KiB
TypeScript

import { withPayload } from "@payloadcms/next/withPayload";
import type { NextConfig } from "next";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(__filename);
// Applied to every route, including /admin and /api — none of these
// constrain what the page can load/render, so they can't break the Payload
// admin bundle. Camera/microphone/display-capture are allowed for the
// site's own origin because the WebRTC call pages (app/(call)/**) need them.
const baseSecurityHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(self), microphone=(self), display-capture=(self), geolocation=(), payment=()" },
// Browsers only ever honor this over an actual HTTPS connection, so it's
// harmless to send in local http:// dev too.
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
];
// Deliberately NOT applied to /admin or /api: Payload's admin bundle isn't
// audited here for CSP compatibility, and API routes serve data, not HTML,
// so a content policy doesn't apply to them anyway. connect-src stays broad
// (rather than 'self') because the WebRTC call pages connect to whatever
// STUN/TURN server is configured via env vars at runtime (see
// lib/meeting/iceServers.ts) — that host isn't known here. style-src needs
// 'unsafe-inline' because framer-motion drives its animations through
// inline style attributes. script-src allows 'unsafe-eval' only in dev,
// matching Next's own documented CSP guidance — React uses eval there for
// server-error-stack reconstruction; neither React nor Next use it in a
// production build.
const isDev = process.env.NODE_ENV === "development";
const frontendCSP = [
"default-src 'self'",
`script-src 'self'${isDev ? " 'unsafe-eval'" : ""}`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",
"font-src 'self' data:",
"connect-src 'self' https: wss: stun: turn: turns:",
"media-src 'self' blob:",
"frame-ancestors 'self'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
"upgrade-insecure-requests",
].join("; ");
const nextConfig: NextConfig = {
// NOT "standalone": this project uses a custom server (server.ts) for the
// WebRTC signaling WebSocket, and standalone output can't be combined with
// a custom server. The Dockerfile runs the full app via `npm start` instead.
images: {
localPatterns: [
{
pathname: "/api/media/file/**",
},
],
},
// Two root layouts exist — app/(frontend) and app/(payload) — so a single
// not-found.tsx can't compose a 404; see app/global-not-found.tsx.
experimental: {
globalNotFound: true,
},
turbopack: {
root: path.resolve(dirname),
},
async headers() {
return [
{ source: "/:path*", headers: baseSecurityHeaders },
{ source: "/((?!admin|api).*)", headers: [{ key: "Content-Security-Policy", value: frontendCSP }] },
];
},
};
export default withPayload(nextConfig, { devBundleServerPackages: false });