Jak vzniká XSS přes JSON
Shrnutí: JSON se sám nikdy nevykonává. XSS vzniká ve chvíli, kdy ho ošetříte jako HTML – spojováním řetězců na serveru, frontendovým innerHTML nebo vložením JSON přímo do značky skriptu.

Stručně: JSON se sám nikdy nevykonává – nejprve uzavřete tři výstupní cesty: escapujte na serveru podle kontextu, používejte textContent místo innerHTML a při vkládání JSON nahraďte < za \u003c.
{"name": "<img src=x onerror=alert(1)>"} je platná data. Zda jsou nebezpečná, záleží výhradně na tom, kam je umístíte.
Vstup první: sestavování řetězců na serveru
res.send(`<div>Hello, ${data.name}</div>`);
Prohlížeč parsuje značky uvnitř data.name. JSON je tady jen kontejner; chybou je escapování pro špatný kontext.
Oprava: použijte výchozí escapování šablonovacího enginu (<%= %> v EJS, {{ }} ve Vue a Django). Vyhněte se <%- %>, v-html a |safe pro cokoli, co ovládá uživatel.
Vstup druhý: innerHTML
el.innerHTML = user.bio; // parsuje značky
el.textContent = user.bio; // vkládá text
Reactí {user.bio} a Vue {{ user.bio }} vytvářejí textové uzly a jsou ve výchozím nastavení bezpečné. dangerouslySetInnerHTML a v-html jsou explicitní výjimky.
Pokud bohatý text skutečně potřebujete, sanitizujte pomocí allowlistu (DOMPurify a podobné), místo abyste důvěřovali backendu.
Vstup třetí: vložení do značky skriptu
<script>
window.__DATA__ = {"name": "x</script><script>alert(1)</script>"};
</script>
</script> uvnitř dat uzavře blok předčasně a všechno za ním se spustí jako nový skript. To je nejvíce opomíjený případ.
JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
Stále platný JSON, po parsování identický, už nemůže uzavřít značku. Next.js a Nuxt dělají totéž pro __NEXT_DATA__ a __NUXT__.
Dva sousedé, které stojí za to znát
JSON hijacking — staré prohlížeče vykonávaly JSON načtený přes <script src>; předefinování Array prozradilo obsah. Moderní prohlížeče to opravily a Content-Type: application/json plus X-Content-Type-Options: nosniff uzavírá zbytek.
Zbytky JSONP — uživatelsky ovládaný název callbacku je spuštění libovolného kódu z principu. V nových systémech ho nepoužívejte.
Kontrolní seznam
- Každé místo, kde pole JSON dorazí do HTML, je escapované pro svůj kontext (HTML, atribut, JS, URL)
- Žádné
innerHTML,v-htmlanidangerouslySetInnerHTMLkonzumující řetězce z API - Vložený JSON escapuje
<,>a& - Bohatý text prochází sanitizací allowlistem, nikoli filtrováním blacklistem
- Odpovědi nesou
Content-Type: application/jsonanosniff
Časté dotazy
Proč platný JSON může stále spustit XSS?
JSON je jen text a nikdy se nevykonává; to, že je platný, znamená jen, že se parsuje. Nebezpečí se objeví, když se zapíše do HTML nebo skriptového kontextu: prohlížeč parsuje značky, nebo </script> uzavře blok předčasně a zbytek se spustí jako kód. Chybou je chybějící escapování respektující kontext na výstupu, nikoli datový formát.
Mám pro vykreslení polí z API použít textContent, nebo innerHTML?
Preferujte textContent: vkládá hodnotu jako prostý text, takže značky se zobrazí doslovně. innerHTML parsuje značky, takže pole z API obsahující <img onerror=...> se vykoná. Reactí {user.bio} a Vue {{ user.bio }} ve výchozím nastavení vytvářejí textové uzly; dangerouslySetInnerHTML a v-html jsou explicitní výjimky.
Jak escapovat JSON vložený do značky <script>?
Ano. </script> uvnitř dat uzavře blok předčasně a všechno za ním se spustí jako nový skript. Po serializaci nahraďte <, > a & za \u003c, \u003e a \u0026: výsledek je stále platný JSON, parsuje se identicky a už nemůže uzavřít značku.