Mitiga il cross-site scripting (XSS) applicando un criterio di sicurezza del contenuto (CSP) rigoroso.

Lukas Weichselbaum
Lukas Weichselbaum

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Il cross-site scripting (XSS), la capacità di inserire script dannosi in un'app web, è stata una delle maggiori vulnerabilità della sicurezza web per oltre un decennio.

La Content Security Policy (CSP) è un livello di sicurezza aggiuntivo che contribuisce a mitigare gli attacchi XSS. Per configurare una CSP, aggiungi l'intestazione HTTP Content-Security-Policy a una pagina web e imposta i valori che controllano le risorse che lo user agent può caricare per quella pagina.

Questa pagina spiega come utilizzare un CSP basato su nonce o hash per mitigare gli XSS, anziché i CSP basati su liste consentite di host di uso comune che spesso lasciano la pagina esposta agli XSS perché possono essere bypassati nella maggior parte delle configurazioni.

Termine chiave: un nonce è un numero casuale utilizzato una sola volta che puoi utilizzare per contrassegnare un tag <script> come attendibile.

Termine chiave: una funzione hash è una funzione matematica che converte un valore di input in un valore numerico compresso chiamato hash. Puoi utilizzare un hash (ad esempio SHA-256) per contrassegnare un tag <script> incorporato come attendibile.

Una Content Security Policy basata su nonce o hash viene spesso chiamata CSP rigida. Quando un'applicazione utilizza una CSP rigida, gli utenti malintenzionati che trovano difetti di HTML injection in genere non possono utilizzarli per forzare il browser a eseguire script dannosi in un documento vulnerabile. Questo perché la CSP rigida consente solo script con hash o script con il valore nonce corretto generato sul server, quindi gli utenti malintenzionati non possono eseguire lo script senza conoscere il nonce corretto per una determinata risposta.

Perché dovresti utilizzare una CSP rigida?

Se il tuo sito ha già un CSP simile a script-src www.googleapis.com, probabilmente non è efficace contro il cross-site. Questo tipo di CSP è chiamato CSP con lista consentita. Richiedono molta personalizzazione e possono essere aggirati dagli utenti malintenzionati.

Le CSP rigide basate su nonce o hash crittografici evitano queste trappole.

Struttura CSP rigida

Una Content Security Policy di base rigorosa utilizza una delle seguenti intestazioni della risposta HTTP:

CSP rigida basata su nonce

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
Come funziona una CSP rigida basata su nonce.

CSP rigida basata su hash

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Le seguenti proprietà rendono un CSP come questo "rigido" e quindi sicuro:

  • Utilizza nonce 'nonce-{RANDOM}' o hash 'sha256-{HASHED_INLINE_SCRIPT}' per indicare quali tag <script> lo sviluppatore del sito ritiene attendibili per l'esecuzione nel browser dell'utente.
  • Imposta 'strict-dynamic' per ridurre l'impegno necessario per il deployment di un CSP basato su nonce o hash consentendo automaticamente l'esecuzione degli script creati da uno script attendibile. In questo modo, viene sbloccato anche l'utilizzo della maggior parte delle librerie e dei widget JavaScript di terze parti.
  • Non si basa su liste consentite di URL, quindi non è soggetto ai bypass CSP comuni.
  • Blocca gli script incorporati non attendibili, come i gestori di eventi incorporati o gli URI javascript:.
  • Limita object-src per disattivare i plug-in pericolosi come Flash.
  • Limita base-uri per bloccare l'inserimento di tag <base>. In questo modo, gli utenti malintenzionati non possono modificare le posizioni degli script caricati da URL relativi.

Adotta una CSP rigida

Per adottare una CSP rigida, devi:

  1. Decidi se la tua applicazione deve impostare un CSP basato su nonce o hash.
  2. Copia il CSP dalla sezione Struttura CSP rigorosa e impostalo come intestazione della risposta nell'applicazione.
  3. Rifattorizza i modelli HTML e il codice lato client per rimuovere i pattern incompatibili con la CSP.
  4. Esegui il deployment del CSP.

Puoi utilizzare il controllo Best practice di Lighthouse (v7.3.0 e versioni successive con il flag --preset=experimental) durante questo processo per verificare se il tuo sito ha una CSP e se è abbastanza rigida da essere efficace contro XSS.

Report di Lighthouse che avvisa che non è stato trovato alcun CSP in modalità di applicazione forzata.
Se il tuo sito non ha un CSP, Lighthouse mostra questo avviso.

Passaggio 1: decidi se hai bisogno di una CSP basata su nonce o hash

Ecco come funzionano i due tipi di CSP rigida:

CSP basata su nonce

Con un CSP basato su nonce, generi un numero casuale in fase di runtime, lo includi nel tuo CSP e lo associ a ogni tag di script nella pagina. Un malintenzionato non può includere o eseguire uno script dannoso nella tua pagina, perché dovrebbe indovinare il numero casuale corretto per lo script. Questo funziona solo se il numero non è indovinabile e viene generato di nuovo in fase di runtime per ogni risposta.

Utilizza una CSP basata su nonce per le pagine HTML di cui viene eseguito il rendering sul server. Per queste pagine, puoi creare un nuovo numero casuale per ogni risposta.

CSP basata su hash

Per un CSP basato su hash, l'hash di ogni tag di script incorporato viene aggiunto al CSP. Ogni script ha un hash diverso. Un malintenzionato non può includere o eseguire uno script dannoso nella tua pagina, perché l'hash dello script dovrebbe essere presente nel CSP per poter essere eseguito.

Utilizza un CSP basato su hash per le pagine HTML pubblicate in modo statico o per le pagine che devono essere memorizzate nella cache. Ad esempio, puoi utilizzare un CSP basato su hash per le applicazioni web a pagina singola create con framework come Angular, React o altri, che vengono pubblicate in modo statico senza rendering lato server.

Passaggio 2: imposta un CSP rigido e prepara gli script

Quando imposti una CSP, hai a disposizione alcune opzioni:

  • Modalità solo report (Content-Security-Policy-Report-Only) o modalità di applicazione forzata (Content-Security-Policy). In modalità solo report, la CSP non blocca ancora le risorse, quindi non si rompe nulla sul tuo sito, ma puoi vedere gli errori e ricevere report per tutto ciò che sarebbe stato bloccato. A livello locale, quando imposti la CSP, questo non ha molta importanza, perché entrambe le modalità mostrano gli errori nella console del browser. In ogni caso, la modalità di applicazione forzata può aiutarti a trovare le risorse bloccate dalla bozza della CSP, perché il blocco di una risorsa può far sembrare la pagina danneggiata. La modalità Solo report diventa più utile in un secondo momento del processo (vedi il passaggio 5).
  • Intestazione o tag HTML <meta>. Per lo sviluppo locale, un tag <meta> può essere più comodo per modificare la CSP e vedere rapidamente l'impatto sul sito. Tuttavia:
    • In seguito, quando esegui il deployment della CSP in produzione, ti consigliamo di impostarla come intestazione HTTP.
    • Se vuoi impostare la CSP in modalità solo report, devi impostarla come intestazione, perché i meta tag CSP non supportano la modalità solo report.

Opzione A: CSP basata su nonce

Imposta la seguente intestazione della risposta HTTP Content-Security-Policy nella tua applicazione:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Genera un nonce per CSP

Un nonce è un numero casuale utilizzato una sola volta per caricamento pagina. Un CSP basato su nonce può mitigare gli XSS solo se gli utenti malintenzionati non riescono a indovinare il valore nonce. Un nonce CSP deve essere:

  • Un valore casuale crittograficamente sicuro (idealmente di lunghezza superiore a 128 bit)
  • Generata di recente per ogni risposta
  • Codifica Base64

Ecco alcuni esempi di come aggiungere un nonce CSP nei framework lato server:

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

Aggiungi un attributo nonce agli elementi <script>

Con un CSP basato su nonce, ogni elemento <script> deve avere un attributo nonce che corrisponda al valore nonce casuale specificato nell'intestazione CSP. Tutti gli script possono avere lo stesso nonce. Il primo passaggio consiste nell'aggiungere questi attributi a tutti gli script in modo che la CSP li consenta.

Opzione B: intestazione della risposta CSP basata su hash

Imposta la seguente intestazione della risposta HTTP Content-Security-Policy nella tua applicazione:

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Per più script incorporati, la sintassi è la seguente: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

Carica gli script di origine in modo dinamico

Puoi caricare script di terze parti in modo dinamico utilizzando uno script incorporato.

Un esempio di come incorporare gli script.
Consentito da CSP
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
Per consentire l'esecuzione di questo script, devi calcolare l'hash dello script incorporato e aggiungerlo all'intestazione della risposta CSP, sostituendo il segnaposto {HASHED_INLINE_SCRIPT}. Per ridurre la quantità di hash, puoi unire tutti gli script incorporati in un unico script. Per vedere un esempio pratico, consulta questo esempio e il relativo codice.
Bloccato da CSP
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
CSP blocca questi script perché non sono stati aggiunti in modo dinamico e non hanno un attributo integrity che corrisponde a un'origine consentita.

Considerazioni sul caricamento degli script

L'esempio di script incorporato aggiunge s.async = false per garantire che foo venga eseguito prima di bar, anche se bar viene caricato per primo. In questo snippet, s.async = false non blocca l'analizzatore sintattico durante il caricamento degli script, perché questi vengono aggiunti dinamicamente. Il parser si interrompe solo durante l'esecuzione degli script, come avviene per gli script async. Tuttavia, con questo snippet, tieni presente quanto segue:

  • Uno o entrambi gli script potrebbero essere eseguiti prima che il download del documento sia stato completato. Se vuoi che il documento sia pronto al momento dell'esecuzione degli script, attendi l'evento DOMContentLoaded prima di aggiungere gli script. Se ciò causa un problema di prestazioni perché il download degli script non inizia abbastanza presto, utilizza i tag di precaricamento in precedenza nella pagina.
  • defer = true non fa nulla. Se hai bisogno di questo comportamento, esegui lo script manualmente quando necessario.

Passaggio 3: rifattorizza i modelli HTML e il codice lato client

I gestori di eventi incorporati (come onclick="…", onerror="…") e gli URI JavaScript (<a href="javascript:…">) possono essere utilizzati per eseguire script. Ciò significa che un utente malintenzionato che trova un bug XSS può inserire questo tipo di HTML ed eseguire JavaScript dannoso. Una CSP basata su nonce o hash vieta l'utilizzo di questo tipo di markup. Se il tuo sito utilizza uno di questi pattern, dovrai rifattorizzarli in alternative più sicure.

Se hai attivato CSP nel passaggio precedente, potrai visualizzare le violazioni di CSP nella console ogni volta che CSP blocca un pattern incompatibile.

Report sulle violazioni della CSP nella console per gli sviluppatori di Chrome.
Errori della console per il codice di blocco.

Nella maggior parte dei casi, la correzione è semplice:

Rifattorizza i gestori di eventi incorporati

Consentito da CSP
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
La CSP consente i gestori di eventi registrati utilizzando JavaScript.
Bloccato da CSP
<span onclick="doThings();">A thing.</span>
CSP blocca i gestori di eventi in linea.

Rifattorizza gli URI javascript:

Consentito da CSP
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
La CSP consente i gestori di eventi registrati utilizzando JavaScript.
Bloccato da CSP
<a href="javascript:linkClicked()">foo</a>
La CSP blocca gli URI javascript:.

Rimuovi eval() dal codice JavaScript

Se la tua applicazione utilizza eval() per convertire le serializzazioni di stringhe JSON in oggetti JS, devi rifattorizzare queste istanze in JSON.parse(), che è anche più veloce.

Se non riesci a rimuovere tutti gli utilizzi di eval(), puoi comunque impostare un CSP rigoroso basato su nonce, ma devi utilizzare la parola chiave CSP 'unsafe-eval', che rende la policy leggermente meno sicura.

Puoi trovare questi e altri esempi di questo tipo di refactoring in questo codelab sulla CSP rigida:

(Facoltativo) Passaggio 4: aggiungi i fallback per supportare le versioni precedenti del browser

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Se devi supportare versioni precedenti del browser:

  • L'utilizzo di strict-dynamic richiede l'aggiunta di https: come riserva per le versioni precedenti di Safari. Quando esegui questa operazione:
    • Tutti i browser che supportano strict-dynamic ignorano il valore di riserva https:, quindi questo non ridurrà l'efficacia della policy.
    • Nei browser meno recenti, gli script di origine esterna possono essere caricati solo se provengono da un'origine HTTPS. Questa opzione è meno sicura di un CSP rigoroso, ma impedisce comunque alcune cause comuni di XSS, come le iniezioni di URI javascript:.
  • Per garantire la compatibilità con versioni molto vecchie del browser (più di 4 anni), puoi aggiungere unsafe-inline come opzione di riserva. Tutti i browser recenti ignorano unsafe-inline se è presente un nonce o un hash CSP.
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

Passaggio 5: esegui il deployment della CSP

Dopo aver verificato che la CSP non blocca gli script legittimi nel tuo ambiente di sviluppo locale, puoi eseguirne il deployment in gestione temporanea e poi nell'ambiente di produzione:

  1. (Facoltativo) Esegui il deployment della CSP in modalità solo report utilizzando l'intestazione Content-Security-Policy-Report-Only. La modalità Solo report è utile per testare una modifica che provoca un errore, come un nuovo CSP in produzione, prima di iniziare ad applicare le limitazioni del CSP. In modalità Solo report, la CSP non influisce sul comportamento dell'app, ma il browser genera comunque errori della console e report sulle violazioni quando rileva pattern incompatibili con la CSP, in modo che tu possa vedere cosa non avrebbe funzionato per gli utenti finali. Per saperne di più, consulta l'API Reporting.
  2. Quando hai la certezza che la CSP non interromperà il sito per gli utenti finali, esegui il deployment della CSP utilizzando l'intestazione della risposta Content-Security-Policy. Ti consigliamo di impostare la CSP utilizzando un'intestazione HTTP lato server, perché è più sicura di un tag <meta>. Dopo aver completato questo passaggio, la CSP inizia a proteggere la tua app dagli attacchi XSS.

Limitazioni

In genere, una CSP rigida fornisce un ulteriore livello di sicurezza efficace che contribuisce a mitigare gli attacchi XSS. Nella maggior parte dei casi, il CSP riduce notevolmente la superficie di attacco rifiutando pattern pericolosi come gli URI javascript:. Tuttavia, in base al tipo di CSP che utilizzi (nonce, hash, con o senza 'strict-dynamic'), esistono casi in cui la CSP non protegge l'app:

  • Se utilizzi un nonce per uno script, ma è presente un'injection direttamente nel corpo o nel parametro src dell'elemento <script>.
  • Se sono presenti injection nelle posizioni degli script creati dinamicamente (document.createElement('script')), incluse le funzioni di libreria che creano nodi DOM script in base ai valori dei relativi argomenti. Sono incluse alcune API comuni come .html() di jQuery, nonché .get() e .post() in jQuery < 3.0.
  • Se sono presenti template injection nelle vecchie applicazioni AngularJS. Un malintenzionato che può eseguire l'injection in un modello AngularJS può utilizzarlo per eseguire JavaScript arbitrario.
  • Se la policy contiene 'unsafe-eval', le iniezioni in eval(), setTimeout() e alcune altre API utilizzate raramente.

Gli sviluppatori e gli ingegneri della sicurezza dovrebbero prestare particolare attenzione a questi pattern durante le revisioni del codice e i controlli di sicurezza. Puoi trovare maggiori dettagli su questi casi in Content Security Policy: A Successful Mess Between Hardening and Mitigation.

Per approfondire