JSON Toolkit

Wie XSS durch JSON entsteht

Fazit: JSON führt sich nie selbst aus. XSS entsteht in dem Moment, in dem Sie es als HTML behandeln – serverseitige Stringverkettung, frontendseitiges innerHTML oder JSON, das direkt in ein Script-Tag eingebettet wird.

Fortgeschritten~4 Min. LesezeitVeröffentlicht 2026-10-30Aktualisiert 2026-10-30
Drei Wege, auf denen JSON zu XSS wird, und wie man sie verhindert

Kurz gesagt: JSON führt sich nie selbst aus – schließen Sie zuerst die drei Ausgabepfade: serverseitig nach Kontext escapen, textContent statt innerHTML verwenden und beim Einbetten von JSON < durch \u003c ersetzen.

{"name": "<img src=x onerror=alert(1)>"} ist gültige Daten. Ob es gefährlich ist, hängt vollständig davon ab, wohin Sie es legen.

Einstieg eins: serverseitiger Stringaufbau

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

Der Browser parst die Tags innerhalb von data.name. JSON ist hier nur der Container; der Fehler ist das Escaping für den falschen Kontext.

Behebung: Verwenden Sie das Standard-Escaping einer Template-Engine (<%= %> in EJS, {{ }} in Vue und Django). Vermeiden Sie <%- %>, v-html und |safe für alles, was von Nutzern kontrolliert wird.

Einstieg zwei: innerHTML

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

Reacts {user.bio} und Vues {{ user.bio }} erzeugen Textknoten und sind standardmäßig sicher. dangerouslySetInnerHTML und v-html sind ausdrückliche Opt-outs.

Brauchen Sie wirklich Rich Text, bereinigen Sie mit einer Allowlist (DOMPurify und ähnliche), statt dem Backend zu vertrauen.

Einstieg drei: Einbetten in ein Script-Tag

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

Ein </script> innerhalb der Daten schließt den Block vorzeitig, und alles danach läuft als neues Script. Das ist der am häufigsten übersehene Fall.

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

Weiterhin gültiges JSON, nach dem Parsen identisch, aber nicht mehr in der Lage, das Tag zu schließen. Next.js und Nuxt tun dasselbe für __NEXT_DATA__ und __NUXT__.

Zwei Nachbarn, die man kennen sollte

JSON-Hijacking – alte Browser führten JSON aus, das über <script src> geladen wurde; das Neudefinieren von Array ließ die Inhalte lecken. Moderne Browser haben das behoben, und Content-Type: application/json plus X-Content-Type-Options: nosniff schließt den Rest.

JSONP-Reste – ein von Nutzern kontrollierter Callback-Name ist konstruktionsbedingt beliebige Codeausführung. Verwenden Sie es in neuen Systemen nicht.

Checkliste

  • Jede Stelle, an der ein JSON-Feld HTML erreicht, ist für ihren Kontext escaped (HTML, Attribut, JS, URL)
  • Kein innerHTML, v-html oder dangerouslySetInnerHTML, das API-Strings konsumiert
  • Eingebettetes JSON escaped <, > und &
  • Rich Text läuft durch Allowlist-Bereinigung, nicht durch Blacklist-Filterung
  • Antworten tragen Content-Type: application/json und nosniff

FAQ

Warum kann gültiges JSON trotzdem XSS auslösen?

Das JSON ist nur Text und führt sich nie aus; gültig zu sein heißt nur, dass es parst. Die Gefahr erscheint, wenn es in einen HTML- oder Script-Kontext geschrieben wird: Der Browser parst die Tags, oder ein </script> schließt den Block vorzeitig und der Rest läuft als Code. Der Fehler ist das fehlende kontextbewusste Escaping bei der Ausgabe, nicht das Datenformat.

Sollte ich textContent oder innerHTML verwenden, um API-Felder zu rendern?

Bevorzugen Sie textContent: Es fügt den Wert als Klartext ein, Tags werden also wörtlich angezeigt. innerHTML parst Markup, ein API-Feld mit <img onerror=...> führt also aus. Reacts {user.bio} und Vues {{ user.bio }} erzeugen standardmäßig Textknoten; dangerouslySetInnerHTML und v-html sind ausdrückliche Opt-outs.

Wie sollte ich JSON escapen, das in ein <script>-Tag eingebettet wird?

Ja. Ein </script> innerhalb der Daten schließt den Block vorzeitig, und alles danach läuft als neues Script. Ersetzen Sie nach dem Serialisieren <, > und & durch \u003c, \u003e und \u0026: Das Ergebnis ist weiterhin gültiges JSON, parst identisch und kann das Tag nicht mehr schließen.

Weiterführende Literatur