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

Lukas Weichselbaum
Lukas Weichselbaum

Supporto dei browser

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

Origine

Il cross-site scripting (XSS), ovvero la possibilità di iniettare script dannosi in un'app web, è una delle più grandi vulnerabilità della sicurezza web da oltre un decennio.

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

Questa pagina spiega come utilizzare un CSP basato su nonce o hash per mitigare gli attacchi XSS, anziché i CSP basati su liste consentite per gli host di uso comune che spesso lasciano la pagina esposta agli attacchi XSS perché possono essere aggiranti 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> in linea come attendibile.

Un criterio di sicurezza del contenuto basato su nonce o hash viene spesso chiamato CSP rigoroso. Quando un'applicazione utilizza un CSP rigoroso, gli aggressori che trovano difetti di attacco di tipo HTML solitamente non possono utilizzarli per forzare il browser a eseguire script dannosi in un documento vulnerabile. Questo perché il CSP rigoroso consente solo script sottomessi ad hashing 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 un CSP rigoroso?

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

I CSP rigidi basati su nonce o hash crittografici evitano questi problemi.

Struttura CSP rigorosa

Un criterio Content Security Policy rigoroso di base utilizza una delle seguenti intestazioni di risposta HTTP:

CSP rigoroso basato su nonce

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

CSP rigoroso basato 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 "rigoroso" e quindi sicuro:

  • Utilizza nonce 'nonce-{RANDOM}' o hash 'sha256-{HASHED_INLINE_SCRIPT}' per indicare quali tag <script> lo sviluppatore del sito ritiene attendibili da eseguire nel browser dell'utente.
  • Imposta 'strict-dynamic' per ridurre lo sforzo di implementare un CSP basato su nonce o hash consentendo automaticamente l'esecuzione di script creati da uno script attendibile. Inoltre, consente di sbloccare l'utilizzo della maggior parte dei widget e delle librerie JavaScript di terze parti.
  • Non si basa su liste consentite di URL, quindi non è soggetta ai bypass del CSP più comuni.
  • Blocca gli script in linea non attendibili, come i gestori di eventi in linea o gli URI javascript:.
  • Limita object-src a disattivare plug-in pericolosi come Flash.
  • Limita base-uri a bloccare l'iniezione di tag <base>. In questo modo, gli autori di attacchi non possono modificare le posizioni degli script caricati da URL relativi.

Adotta un CSP rigoroso

Per adottare un CSP rigoroso, devi:

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

Puoi utilizzare il controllo delle best practice di Lighthouse (versione 7.3.0 e successive con il flag --preset=experimental) durante questa procedura per verificare se il tuo sito dispone di un CSP e se è sufficientemente rigoroso per essere efficace contro gli attacchi XSS.

Il report Lighthouse avverte che non è stato trovato alcun criterio 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 un CSP basato su nonce o hash

Ecco come funzionano i due tipi di CSP rigorosi:

CSP basato su nonce

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

Utilizza un CSP basato su nonce per le pagine HTML visualizzate sul server. Per queste pagine, puoi creare un nuovo numero casuale per ogni risposta.

CSP basato su hash

Per un CSP basato su hash, l'hash di ogni tag script in linea 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 deve trovarsi nel tuo 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 applicazioni web di una sola pagina create con framework come Angular, React o altri, che vengono pubblicate in modo statico senza il rendering lato server.

Passaggio 2: imposta un CSP rigoroso e prepara gli script

Quando configuri un CSP, hai a disposizione alcune opzioni:

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

Opzione A: CSP basato su nonce

Imposta la seguente intestazione di 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 il CSP

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

  • Un valore casuale di crittografia sicura (idealmente di almeno 128 bit)
  • Generato di nuovo 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 });
});

Aggiungere 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 random 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 il CSP li consenta.

Opzione B: intestazione di risposta CSP basata su hash

Imposta la seguente intestazione di 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 in linea, la sintassi è la seguente: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

Caricare gli script di origine in modo dinamico

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

Un esempio di come inserire gli script in linea.
Consentito dal 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 in linea e aggiungerlo all'intestazione di risposta CSP, sostituendo il segnaposto {HASHED_INLINE_SCRIPT}. Per ridurre la quantità di hash, puoi unire tutti gli script in linea in un unico script. Per vedere come funziona, consulta questo esempio e il relativo codice.
Bloccato dal 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 dinamicamente e non hanno un attributo integrity che corrisponda a un'origine consentita.

Considerazioni sul caricamento degli script

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

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

Passaggio 3: esegui il refactoring dei modelli HTML e del codice lato client

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

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

Report sulle violazioni del CSP nella console di sviluppo di Chrome.
Errori della console per il codice bloccato.

Nella maggior parte dei casi, la soluzione è semplice:

Ristrutturare i gestori di eventi in linea

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

Ristrutturare gli URI javascript:

Consentito dal CSP
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
CSP consente i gestori di eventi registrati utilizzando JavaScript.
Bloccato dal CSP
<a href="javascript:linkClicked()">foo</a>
Il 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 eseguire il refactoring di queste istanze in JSON.parse(), che è anche più veloce.

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

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

(Facoltativo) Passaggio 4: aggiungi soluzioni di riserva per supportare le versioni precedenti dei browser

Supporto dei browser

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

Origine

Se devi supportare versioni precedenti del browser:

  • L'utilizzo di strict-dynamic richiede l'aggiunta di https: come opzione di riserva per le versioni precedenti di Safari. In questo modo:
    • Tutti i browser che supportano strict-dynamic ignorano il valore alternativo https:, pertanto l'efficacia del criterio non verrà ridotta.
    • Nei browser meno recenti, gli script di origine esterna possono essere caricati solo se provengono da un'origine HTTPS. Questo approccio è meno sicuro di un CSP rigoroso, ma impedisce comunque alcune cause comuni di XSS, come le iniezioni di URI javascript:.
  • Per garantire la compatibilità con versioni del browser molto vecchie (oltre 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 del CSP

Dopo aver verificato che il CSP non blocchi script legittimi nell'ambiente di sviluppo locale, puoi eseguire il deployment del CSP nell'ambiente di staging e poi nell'ambiente di produzione:

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

Limitazioni

In genere, un CSP rigoroso fornisce un ulteriore livello di sicurezza 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 il CSP non protegge anche la tua app:

  • Se inserisci un nonce in uno script, ma è presente un'iniezione direttamente nel corpo o nel parametro src dell'elemento <script>.
  • Se sono presenti iniettazioni nelle posizioni degli script creati dinamicamente (document.createElement('script')), incluse le funzioni della 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 iniettamenti di modelli nelle vecchie applicazioni AngularJS. Un malintenzionato che può eseguire l'injection in un modello AngularJS può utilizzarlo per eseguire JavaScript arbitrario.
  • Se il criterio contiene 'unsafe-eval', le iniezioni in eval(), setTimeout() e in alcune altre API raramente utilizzate.

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

Per approfondire