JSON Toolkit

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.

Intermedio~4 min di letturaPubblicato il 2026-10-30Aggiornato il 2026-10-30
Tre modi in cui JSON diventa XSS e come prevenirli

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-html o dangerouslySetInnerHTML che 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/json e nosniff

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.

Ulteriori letture