Un browser moderno è disposto a far rispettare un sacco di policy di sicurezza per te, ma solo se il server gliele dice. Il modo in cui il server gliele dice sono gli HTTP response header. Se li imposti, il browser blocca gli attacchi. Se te ne dimentichi, li lascia passare e te ne accorgi da un cliente.

Sei header vale la pena conoscerli bene. Andiamo uno per uno: cosa fa, cosa rompi se lo imposti male, un default sensato.

1. Strict-Transport-Security (HSTS)

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

Dice al browser "da ora in poi non collegarti più a questo dominio in HTTP. Se l'URL è http://, riscrivilo in https:// prima di mandare la richiesta, senza nemmeno chiedermelo." Una volta che il browser ha visto questo header, se lo ricorda per max-age secondi (qui: 2 anni).

Blocca una classe di attacchi in cui qualcuno sulla stessa Wi-Fi del tuo visitatore intercetta il redirect e degrada la connessione a HTTP (sslstrip). Senza HSTS la prima connessione di ogni sessione del browser è in HTTP ed è intercettabile. Con HSTS il browser si rifiuta proprio di provarci.

Cosa rompi se lo imposti male: se abiliti HSTS senza che il certificato SSL sia pronto su tutti i sottodomini (e hai usato includeSubDomains), ogni sottodominio diventa irraggiungibile finché il certificato non funziona. Prima testa senza includeSubDomains, poi lo aggiungi quando sei sicuro che tutti i sottodomini abbiano TLS valido.

preload è opzionale e significa "metti il mio dominio nella lista HSTS preload dei binari del browser". Una volta in preload, il browser rifiuta HTTP per il tuo dominio fin dalla primissima visita. La rimozione dalla lista è lenta (mesi). Abilita preload solo quando sei sicuro.

2. Content-Security-Policy (CSP)

Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; script-src 'self'; style-src 'self' 'unsafe-inline'

Dice al browser "per questa pagina carica solo risorse dalle origini che ti elenco. Tutto il resto, rifiutati." È la difesa più efficace contro l'XSS (cross-site scripting). Se sul tuo sito viene iniettato un tag <script> malevolo, la CSP impedisce che venga eseguito perché non viene da una fonte autorizzata.

La CSP è anche l'header che ha più probabilità di rompere il sito se la copi-incolli da un tutorial. Quasi tutti i WordPress usano script inline, stili inline, font da terze parti, analytics da un altro dominio, embed YouTube. Una CSP stretta blocca tutto questo per default.

L'approccio pragmatico:

  1. Inizia con Content-Security-Policy-Report-Only invece di Content-Security-Policy. Stessa sintassi, ma il browser solo registra le violazioni, non blocca. Vedi nella console cosa carica davvero il sito.
  2. Costruisci una policy che permetta solo quello che ti serve. Dominio per dominio.
  3. Passi alla modalità di enforcement quando il log dei violation è pulito.

Il guadagno singolo più grande, anche se non arrivi a una CSP stretta, è frame-ancestors 'self', che impedisce al tuo sito di finire dentro un <iframe> su un altro dominio. Da solo sostituisce il vecchio X-Frame-Options.

3. X-Content-Type-Options

X-Content-Type-Options: nosniff

Dice al browser "fidati del Content-Type che ti mando. Non provare a indovinare." Senza, se il tuo server manda un file come text/plain ma il contenuto sembra HTML o JavaScript, alcuni browser lo eseguono lo stesso. Questo abilita uno schema di attacco in cui un aggressore carica un .txt con dentro HTML o JS, e il browser lo esegue.

Non c'è nessun lato negativo nell'abilitarlo. Mettilo una volta su ogni risposta e dimenticatene. L'header ha un solo valore (nosniff) e basta.

4. Referrer-Policy

Referrer-Policy: strict-origin-when-cross-origin

Controlla cosa viene mandato nell'header Referer (sì, scritto male, ragioni storiche) quando qualcuno clicca un link dalla tua pagina a un altro sito. Il comportamento di default del browser passa l'URL completo, comprese le query string, che spesso contengono token di sessione, user ID, query di ricerca.

strict-origin-when-cross-origin è un default bilanciato: se l'utente naviga dentro il tuo dominio, manda l'URL completo; se naviga verso un altro dominio, manda solo l'origine (https://example.com, senza path); se naviga da HTTPS a HTTP, non manda niente.

Se il tuo sito ha dati sensibili negli URL (non dovrebbe, ma capita), restringi ancora a strict-origin o anche no-referrer.

5. Permissions-Policy

Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()

Dice al browser quali API sensibili il tuo sito è autorizzato a usare. Le parentesi vuote significano "nessuno, neanche le mie pagine". Il default è "chiunque". L'header è verboso e c'è una lista lunga di feature (payment, usb, serial, clipboard-read, display-capture, fullscreen, decine).

Perché vale la pena: molte di queste API mostrano un prompt di permessi all'utente, e uno script malevolo iniettato sulla tua pagina (o un plugin con bug) può spammare quei prompt. Permissions-Policy ti permette di dire "il mio sito non usa mai il microfono, quindi il browser non deve nemmeno considerarlo, anche se uno script lo chiede".

Default pratico: nega tutto quello che non usi davvero. Se il sito non ha videochiamate, mappe, feature PWA, la riga qui sopra con tutto fra () è una buona base. Aggiungi origini specifiche quando una feature serve.

6. X-Frame-Options (deprecato, ma utile per browser vecchi)

X-Frame-Options: SAMEORIGIN

Equivalente più vecchio di frame-ancestors della CSP. Dice "non permettere alle mie pagine di finire dentro un iframe su un'altra origine". Due valori contano in pratica: DENY (nessun embedding) e SAMEORIGIN (solo il tuo dominio può embeddare le tue pagine).

I browser moderni rispettano frame-ancestors della CSP e ignorano X-Frame-Options se ci sono entrambi. Ma non tutti i bot, le versioni di browser, gli scanner lo fanno. Tenere X-Frame-Options: SAMEORIGIN accanto a frame-ancestors 'self' non costa niente e copre i client più vecchi.

Un blocco nginx di partenza

Default sensato. Mettilo nel server block, o in uno snippet che includi da tutti i siti:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; frame-ancestors 'self'" always;

Il flag always è importante. Senza, nginx salta l'header sulle risposte con status code fuori dalla lista di default (4xx, 5xx, 1xx). I security header li vuoi anche sulle pagine di errore.

Come verificare

Apri DevTools, scheda Network, clicca la richiesta del documento, guarda la sezione Response Headers. Oppure usa uno di questi tester online:

  • securityheaders.com (Scott Helme)
  • observatory.mozilla.org
  • internet.nl

Ti danno un report con voti e ti spiegano esattamente cosa manca o cosa è impostato male. La prima passata su un sito tipico senza header prende una F. Dopo aver aggiunto il blocco qui sopra e aver stretto la CSP gradualmente, arrivare a una A è realistico.

Da cosa non ti proteggono

Gli header sono necessari, non sufficienti. Bloccano specifici attacchi a livello browser. Non bloccano:

  • SQL injection (è responsabilità dell'applicazione)
  • Brute force sul login (rate limiting e 2FA)
  • Plugin di terze parti compromessi (audit e aggiornamenti)
  • Server-side request forgery (validazione input)
  • DDoS (una CDN con rate limiting)

Imposta gli header lo stesso. Costano dieci minuti una volta sola e ripagano ogni giorno dopo.