Docs/Sicurezza
// sicurezza
Security headers
Gli header di sicurezza sono la difesa col miglior rapporto tra minuti spesi e rischio tolto: una manciata di righe in next.config.js e chiudi clickjacking, MIME sniffing e fuga di referrer verso terzi. In ZeroToShip non sono ancora configurati: il file oggi ha solo reactStrictMode, images e webpack.
Il file di oggi
Prima di modificarlo, ecco cosa c'è dentro adesso — così sai cosa non devi rompere:
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'lh3.googleusercontent.com' },
{ protocol: 'https', hostname: 'pbs.twimg.com' },
{ protocol: 'https', hostname: 'images.unsplash.com' },
{ protocol: 'https', hostname: 'logos-world.net' },
],
},
webpack: (config, { webpack, isServer }) => { /* ... */ },
};
module.exports = nextConfig;La versione con gli header
Si aggiunge una funzione async headers() accanto alle chiavi esistenti. Questo è il file completo, pronto da incollare:
/** @type {import('next').NextConfig} */
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=(), browsing-topics=()',
},
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
];
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'lh3.googleusercontent.com',
},
{
protocol: 'https',
hostname: 'pbs.twimg.com',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: 'logos-world.net',
},
],
},
async headers() {
return [
{
// vale per ogni percorso del sito
source: '/:path*',
headers: securityHeaders,
},
];
},
webpack: (config, { webpack, isServer }) => {
// Ignore MongoDB's optional dependencies to prevent build warnings
if (isServer) {
config.plugins.push(
new webpack.IgnorePlugin({
resourceRegExp: /^(kerberos|@mongodb-js\/zstd|@aws-sdk\/credential-providers|gcp-metadata|snappy|socks|aws4|mongodb-client-encryption)$/,
})
);
}
return config;
},
};
module.exports = nextConfig;Modificare next.config.js richiede un riavvio di npm run dev: gli header non si applicano a caldo.
Cosa fa ognuno
- Strict-Transport-Security: dice al browser di parlare con te solo in HTTPS per i prossimi due anni, anche se l'utente digita
http://. Chiude gli attacchi di downgrade. Attenzione: conincludeSubDomainsvale per tutti i sottodomini — se ne hai uno solo in HTTP, si rompe. Epreloadè quasi irreversibile, mettilo solo quando sei sicuro. - X-Frame-Options: DENY: nessuno può mettere il tuo sito dentro un
iframe. Toglie di mezzo il clickjacking (una pagina finta sopra la tua che ti fa cliccare dove vuole lei). In questo repo è sicuro: il checkout Stripe è un redirect —components/ButtonCheckout.tsxfawindow.location.href = url— non un iframe. - X-Content-Type-Options: nosniff: il browser deve fidarsi del
Content-Typeche gli mandi, senza tirare a indovinare. Impedisce che un file caricato dagli utenti venga interpretato come JavaScript. - Referrer-Policy: con
strict-origin-when-cross-origini siti esterni vedono solo il tuo dominio, non il path completo. Senza, un link in uscita da una pagina tipo/dashboard/ordine/1234regala quell'URL a chi sta dall'altra parte. - Permissions-Policy: spegne API del browser che non usi (fotocamera, microfono, posizione). Se un giorno ti finisce dentro uno script di terze parti compromesso, non può nemmeno provare a chiederle.
- X-DNS-Prefetch-Control: non è sicurezza, è performance — risolve in anticipo i DNS dei domini linkati.
La Content-Security-Policy
È l'header più potente e l'unico che può spaccarti il sito, quindi va per ultimo. Prima devi sapere cosa carica davvero questa app:
- datafa.st — lo script di analytics montato in
app/layout.tsxconnext/script. - Crisp —
components/LayoutClient.tsxinietta il widget, ma solo seconfig.crisp.idè valorizzato. Nelconfig.tsattuale è vuoto: se non usi Crisp, non serve metterlo in policy. - Supabase — chiamate REST e WebSocket verso il tuo progetto
*.supabase.co. - Font — nessun dominio esterno.
next/font/googlescarica Inter, JetBrains Mono e Instrument Serif al build e li serve dal tuo dominio. - Immagini remote — gli stessi host che trovi in
images.remotePatterns.
Parti in modalità report only: il browser ti segnala in console tutto ciò che verrebbe bloccato, senza bloccare niente. Aggiungi questo alla lista degli header:
const csp = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://datafa.st",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https://lh3.googleusercontent.com https://pbs.twimg.com https://images.unsplash.com https://logos-world.net",
"font-src 'self' data:",
"connect-src 'self' https://*.supabase.co wss://*.supabase.co https://datafa.st",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; ');
// ...poi dentro securityHeaders:
{ key: 'Content-Security-Policy-Report-Only', value: csp },Navighi il sito, guardi la console, aggiusti le direttive. Quando non ti segnala più niente, togli -Report-Only dalla chiave e la policy diventa attiva.
Quel 'unsafe-inline' su script-src indebolisce parecchio la protezione: serve perché Next.js inietta script inline per l'hydration. Per toglierlo davvero serve un nonce generato a ogni richiesta, e il posto dove generarlo è middleware.ts — che in questo repo esiste già e gira su ogni pagina:
export async function middleware(request: NextRequest) {
const response = await updateSession(request);
const nonce = crypto.randomUUID();
response.headers.set(
"Content-Security-Policy",
[
"default-src 'self'",
"script-src 'self' 'nonce-" + nonce + "' 'strict-dynamic'",
"frame-ancestors 'none'",
"base-uri 'self'",
].join("; ")
);
return response;
}Poi ogni tag <Script> deve ricevere quel nonce. È lavoro vero: fallo quando il resto è a posto, non al primo giorno.
Verifica che ci siano
curl -sI https://zerotoship.com | grep -iE 'x-frame|content-type-options|referrer|strict-transport|content-security'In locale gli header ci sono ma Strict-Transport-Security il browser lo ignora, perché su localhost non sei in HTTPS. Normale.