Come avviene l'XSS tramite JSON
In sintesi: JSON non viene mai eseguito da solo. L'XSS avviene nel momento in cui lo tratti come HTML — concatenazione di stringhe lato server, innerHTML lato frontend, o incorporamento diretto di JSON dentro un tag script.

In sintesi: JSON non viene mai eseguito da solo — chiudi prima i tre percorsi di output: escape lato server in base al contesto, usa textContent invece di innerHTML, e sostituisci < con \u003c quando incorpori JSON.
{"name": "<img src=x onerror=alert(1)>"} è un dato valido. Se sia pericoloso dipende interamente da dove lo metti.
Ingresso uno: costruzione di stringhe lato server
res.send(`<div>Hello, ${data.name}</div>`);
Il browser analizza i tag dentro data.name. Qui JSON è solo il contenitore; la colpa è dell'escaping per il contesto sbagliato.
Correzione: usa l'escaping predefinito di un motore di template (<%= %> in EJS, {{ }} in Vue e Django). Evita <%- %>, v-html e |safe per qualsiasi cosa controllata dall'utente.
Ingresso due: innerHTML
el.innerHTML = user.bio; // parses markup
el.textContent = user.bio; // inserts text
{user.bio} di React e {{ user.bio }} di Vue creano nodi di testo e sono sicuri per impostazione predefinita. dangerouslySetInnerHTML e v-html sono opt-out espliciti.
Se hai davvero bisogno di rich text, sanitizza con una allowlist (DOMPurify e simili) invece di fidarti del backend.
Ingresso tre: incorporamento dentro un tag script
<script>
window.__DATA__ = {"name": "x</script><script>alert(1)</script>"};
</script>
Un </script> dentro i dati chiude presto il blocco e tutto ciò che segue viene eseguito come un nuovo script. È il caso più trascurato.
JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
Ancora JSON valido, identico dopo il parsing, non più in grado di chiudere il tag. Next.js e Nuxt fanno lo stesso per __NEXT_DATA__ e __NUXT__.
Due vicini che vale la pena conoscere
Dirottamento JSON — i browser vecchi eseguivano JSON caricato tramite <script src>; ridefinire Array faceva trapelare il contenuto. I browser moderni l'hanno risolto, e Content-Type: application/json più X-Content-Type-Options: nosniff chiude il resto.
Residui di JSONP — un nome di callback controllato dall'utente è esecuzione di codice arbitrario per design. Non usarlo nei nuovi sistemi.
Lista di controllo
- Ogni punto in cui un campo JSON raggiunge HTML viene sottoposto a escape per il suo contesto (HTML, attributo, JS, URL)
- Nessun
innerHTML,v-htmlodangerouslySetInnerHTMLche consuma stringhe dell'API - Il JSON incorporato fa escape di
<,>e& - Il rich text passa attraverso la sanitizzazione con allowlist, non il filtraggio con blacklist
- Le risposte portano
Content-Type: application/jsonenosniff
FAQ
Perché JSON valido può comunque innescare XSS?
Il JSON è solo testo e non viene mai eseguito; essere valido significa solo che viene analizzato. Il pericolo compare quando viene scritto in un contesto HTML o script: il browser analizza i tag, oppure un </script> chiude presto il blocco e il resto viene eseguito come codice. La colpa è dell'escaping consapevole del contesto mancante in output, non del formato dei dati.
Devo usare textContent o innerHTML per renderizzare i campi dell'API?
Preferisci textContent: inserisce il valore come testo semplice, quindi i tag vengono mostrati letteralmente. innerHTML analizza il markup, quindi un campo dell'API che contiene <img onerror=...> viene eseguito. {user.bio} di React e {{ user.bio }} di Vue creano nodi di testo per impostazione predefinita; dangerouslySetInnerHTML e v-html sono opt-out espliciti.
Come devo fare escape del JSON incorporato in un tag <script>?
Sì. Un </script> dentro i dati chiude presto il blocco e tutto ciò che segue viene eseguito come un nuovo script. Dopo la serializzazione, sostituisci <, > e & con \u003c, \u003e e \u0026: il risultato è ancora JSON valido, si analizza allo stesso modo, e non può più chiudere il tag.