JSON Toolkit

Jak dochodzi do XSS przez JSON

Podsumowanie: JSON nigdy nie wykonuje się sam. XSS powstaje w momencie, gdy traktujesz go jak HTML — składanie łańcuchów po stronie serwera, we frontendowy innerHTML albo osadzenie JSON-a prosto w tagu skryptu.

Średniozaawansowany~4 min czytaniaOpublikowano 2026-10-30Zaktualizowano 2026-10-30
Trzy sposoby, w jakie JSON staje się XSS, i jak im zapobiec

Najważniejsze: JSON nigdy nie wykonuje się sam — najpierw zamknij trzy ścieżki wyjścia: escape'uj po stronie serwera według kontekstu, używaj textContent zamiast innerHTML i zastąp < przez \u003c przy osadzaniu JSON.

{"name": "<img src=x onerror=alert(1)>"} to poprawna dana. To, czy jest niebezpieczna, zależy całkowicie od tego, gdzie ją umieścisz.

Wejście pierwsze: budowanie łańcuchów po stronie serwera

res.send(`<div>Hello, ${data.name}</div>`);

Przeglądarka parsuje znaczniki wewnątrz data.name. JSON jest tu tylko kontenerem; winą jest ucieczka dla złego kontekstu.

Poprawka: użyj domyślnej ucieczki silnika szablonów (<%= %> w EJS, {{ }} w Vue i Django). Unikaj <%- %>, v-html i |safe dla czegokolwiek kontrolowanego przez użytkownika.

Wejście drugie: innerHTML

el.innerHTML = user.bio;    // parses markup
el.textContent = user.bio;  // inserts text

{user.bio} w React i {{ user.bio }} w Vue tworzą węzły tekstowe i są domyślnie bezpieczne. dangerouslySetInnerHTML i v-html to jawne rezygnacje.

Jeśli naprawdę potrzebujesz tekstu sformatowanego, sanityzuj z białą listą (DOMPurify i podobne), zamiast ufać backendowi.

Wejście trzecie: osadzenie w tagu skryptu

<script>
  window.__DATA__ = {"name": "x</script><script>alert(1)</script>"};
</script>

</script> wewnątrz danych zamyka blok wcześniej, a wszystko po nim działa jako nowy skrypt. To najbardziej przeoczany przypadek.

JSON.stringify(data)
  .replace(/</g, '\\u003c')
  .replace(/>/g, '\\u003e')
  .replace(/&/g, '\\u0026');

Nadal poprawny JSON, identyczny po parsowaniu, nie potrafi już zamknąć tagu. Next.js i Nuxt robią to samo dla __NEXT_DATA__ i __NUXT__.

Dwaj sąsiedzi warci poznania

JSON hijacking — stare przeglądarki wykonywały JSON ładowany przez <script src>; przedefiniowanie Array ujawniało zawartość. Współczesne przeglądarki to naprawiły, a Content-Type: application/json plus X-Content-Type-Options: nosniff zamyka resztę.

Pozostałości JSONP — nazwa callbacku kontrolowana przez użytkownika to z założenia wykonanie dowolnego kodu. Nie używaj tego w nowych systemach.

Lista kontrolna

  • Każde miejsce, w którym pole JSON dociera do HTML, jest escape'owane według kontekstu (HTML, atrybut, JS, URL)
  • Żadnego innerHTML, v-html ani dangerouslySetInnerHTML konsumującego łańcuchy z API
  • Osadzony JSON escape'uje <, > i &
  • Tekst sformatowany przechodzi sanitację białą listą, a nie filtrowanie czarną listą
  • Odpowiedzi niosą Content-Type: application/json i nosniff

FAQ

Dlaczego poprawny JSON nadal może wywołać XSS?

JSON to tylko tekst i nigdy się nie wykonuje; bycie poprawnym znaczy jedynie, że się parsuje. Niebezpieczeństwo pojawia się, gdy jest zapisywany w kontekście HTML lub skryptu: przeglądarka parsuje znaczniki albo </script> zamyka blok wcześniej i reszta działa jako kod. Winą jest brak ucieczki dostosowanej do kontekstu przy wyjściu, a nie format danych.

Czy użyć textContent czy innerHTML do renderowania pól API?

Preferuj textContent: wstawia wartość jako zwykły tekst, więc znaczniki są wyświetlane dosłownie. innerHTML parsuje znaczniki, więc pole API zawierające <img onerror=...> się wykona. {user.bio} w React i {{ user.bio }} w Vue domyślnie tworzą węzły tekstowe; dangerouslySetInnerHTML i v-html to jawne rezygnacje.

Jak escape'ować JSON osadzony w tagu <script>?

Tak. </script> wewnątrz danych zamyka blok wcześniej i wszystko po nim działa jako nowy skrypt. Po serializacji zastąp <, > i & przez \u003c, \u003e i \u0026: wynik nadal jest poprawnym JSON-em, parsuje się identycznie i nie potrafi już zamknąć tagu.

Dodatkowe materiały