ZeroToShip

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:

next.config.js
js
/** @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:

next.config.js
js
/** @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: con includeSubDomains vale per tutti i sottodomini — se ne hai uno solo in HTTP, si rompe. E preload è 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.tsx fa window.location.href = url — non un iframe.
  • X-Content-Type-Options: nosniff: il browser deve fidarsi del Content-Type che gli mandi, senza tirare a indovinare. Impedisce che un file caricato dagli utenti venga interpretato come JavaScript.
  • Referrer-Policy: con strict-origin-when-cross-origin i siti esterni vedono solo il tuo dominio, non il path completo. Senza, un link in uscita da una pagina tipo /dashboard/ordine/1234 regala 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.tsx con next/script.
  • Crisp — components/LayoutClient.tsx inietta il widget, ma solo se config.crisp.id è valorizzato. Nel config.ts attuale è 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/google scarica 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:

next.config.js
js
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:

middleware.ts
ts
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

terminal
bash
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.