HTML, Attribute, JavaScript, URL und CSS brauchen jeweils eigene Regeln
Ein einziges universelles Escaping bringt Magento- und Hyvä-Shops in Gefahr, weil HTML, Attribute, JavaScript, URLs und CSS jeweils eigene Syntaxregeln und eigene Angriffsvektoren haben. Dieser Artikel zeigt, warum htmlspecialchars allein nicht reicht, wie die Hyvä-Escaper-Klasse den passenden Kontext trifft und wie Content-Security-Policy als zusätzliche Verteidigungslinie wirkt.
Inhaltsverzeichnis
- 1. Warum eine universelle Escaping-Funktion falsch ist
- 2. Die fünf Ausgabekontexte: HTML, Attribut, JavaScript, URL, CSS
- 3. htmlspecialchars(): Die klassischen Stolperfallen
- 4. Contextual Auto-Escaping in Templating-Engines
- 5. Die Magento/Hyvä Escaper-Klasse im Detail
- 6. Doppelt-Encoding: Wenn Escaping zu oft angewendet wird
- 7. DOM-based XSS: innerHTML vs. textContent
- 8. Content-Security-Policy als zusätzliche Verteidigungslinie
- 9. Escaping in der Praxis: Code-Review-Checkliste
- 10. Zusammenfassung
- 11. FAQ
1. Warum eine universelle Escaping-Funktion falsch ist
Viele Entwickler behandeln XSS-Schutz als abgehaktes Kästchen: Jede dynamische Ausgabe wird durch eine einzige Funktion geschickt, meist htmlspecialchars(), und das Thema gilt als erledigt. Dieses Denken ignoriert, dass Escaping kein einzelner Algorithmus ist, sondern von der Syntax des Zielkontexts abhängt. Ein Wert, der sicher in HTML-Body-Text steht, kann in einem JavaScript-String, einer URL oder einem CSS-Wert trotzdem eine Ausführung fremden Codes ermöglichen, weil dort völlig andere Zeichen gefährlich sind.
Der Begriff Output-Encoding beschreibt genau diese kontextspezifische Umwandlung: Steuerzeichen des Zielformats werden so kodiert, dass sie vom Parser als Daten statt als Code interpretiert werden. HTML kennt &, < und >, Attribute zusätzlich Anführungszeichen, JavaScript-Strings Backslashes und Quotes, URLs reservierte Zeichen nach RFC 3986, CSS Steuerzeichen über Backslash-Hex-Sequenzen. Jede dieser Regeln ist unterschiedlich, teils sogar widersprüchlich zueinander.
Für ein Code-Review bedeutet das: Die Frage ist nie nur, ob escaped wird, sondern ob mit der richtigen Methode für genau diesen Ausgabeort escaped wird. Die folgenden Abschnitte gehen die fünf wichtigsten Kontexte durch, zeigen typische htmlspecialchars()-Fehler, stellen die Hyvä-Escaper-Klasse vor und ordnen Content-Security-Policy als zusätzliche, aber niemals alleinige Verteidigungslinie ein.
2. Die fünf Ausgabekontexte: HTML, Attribut, JavaScript, URL, CSS
HTML-Body-Text ist der einfachste Kontext: Nur &, < und > müssen kodiert werden, weil sie Markup einleiten. Sobald der Wert innerhalb eines Attributs steht, kommen Anführungszeichen dazu, denn ein unescaptes " oder ' beendet das Attribut vorzeitig, und alles danach wird als neues Attribut oder Markup interpretiert, selbst wenn kein < im Wert vorkommt.
Ein JavaScript-String-Kontext hat eine komplett andere Gefahrenmenge: Backslash, Anführungszeichen, Zeilenumbrüche und die Zeichenfolge, die einen umschließenden Inline-Script-Block beendet, müssen JS-spezifisch kodiert werden. HTML-Entities helfen hier nicht, weil der JavaScript-Parser sie nicht auflöst, sondern als reinen Text übernimmt. URLs folgen wiederum eigenen Regeln nach RFC 3986: Reservierte Zeichen wie &, ? oder # werden prozentkodiert, zusätzlich muss das Schema geprüft werden, weil javascript: als Pseudo-Protokoll Code ausführen kann, ganz ohne ein Skript-Tag im Markup.
CSS-Werte schließlich benötigen Backslash-Hex-Escaping, weil expression() in älteren Browsern und url()-Werte mit eingebettetem JavaScript historisch Angriffsvektoren waren. Wer diese fünf Kontexte nicht sauber trennt, wendet zwangsläufig irgendwo die falsche Kodierung an, meist ohne dass ein Funktionstest das bemerkt, weil der Code auf den ersten Blick fehlerfrei funktioniert.
3. htmlspecialchars(): Die klassischen Stolperfallen
htmlspecialchars() ist die Standardfunktion für HTML-Escaping in PHP, aber die Standardwerte ändern sich zwischen PHP-Versionen und werden regelmäßig falsch angenommen. Vor PHP 8.1 kodierte die Funktion ohne explizites Flag nur doppelte Anführungszeichen, einfache blieben unangetastet. Ein Attribut, das mit einfachen Anführungszeichen geschrieben ist, ließ sich damit trotz Escaping durchbrechen. Seit PHP 8.1 ist ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 Standard, verlassen sollte man sich darauf trotzdem nie, weil Codebasen selten auf einer einzigen PHP-Version bleiben.
Das zweite Problem ist das fehlende Charset-Argument. Ohne explizite Angabe von UTF-8 interpretiert htmlspecialchars() den String je nach PHP-Konfiguration unterschiedlich, was bei Sonderzeichen zu inkonsistentem oder fehlerhaftem Escaping führt, etwa wenn Multi-Byte-Zeichen mitten im Zeichen zerschnitten werden. Die korrekte Signatur lautet daher immer htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8').
Der dritte und häufigste Fehler ist der Kontext selbst: htmlspecialchars() kodiert ausschließlich für HTML. Wird der Rückgabewert in einen JavaScript-String, eine URL oder einen CSS-Wert eingebettet, bleibt die eigentliche Injection-Lücke offen, weil dort völlig andere Zeichen gefährlich sind. Das folgende Beispiel zeigt beide Fehlerklassen und die korrekte Alternative.
<?php
declare(strict_types=1);
// WRONG: no ENT_QUOTES, no explicit charset - single-quoted attributes stay unescaped
$name = "O'Brien\" onmouseover=\"alert(1)";
echo '<input type="text" value="' . htmlspecialchars($name) . '">';
// Rendered: <input type="text" value="O'Brien" onmouseover="alert(1)">
// The single quote around O'Brien breaks out of the double-quoted HTML
// attribute in older default flag combinations - never rely on defaults.
// WRONG: HTML-escaping a value does not make it safe for a JS string context
$search = '"; alert(document.cookie); //';
$jsUnsafe = htmlspecialchars($search);
// Embedding $jsUnsafe inside an inline JS string still allows the payload
// to break out, because HTML entities are meaningless inside JavaScript.
// RIGHT: explicit flags and charset for a genuine HTML attribute
$safeAttr = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo '<input type="text" value="' . $safeAttr . '">';
// RIGHT: for a JS string context, encode for JS - json_encode with hex flags
$safeJs = json_encode($search, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
// $safeJs can now be embedded directly after "var q = " in an inline script block
4. Contextual Auto-Escaping in Templating-Engines
Moderne Templating-Engines wie Twig, Blade oder Latte implementieren kontextbezogenes Auto-Escaping: Der Compiler erkennt beim Parsen des Templates, ob eine Variable in HTML-Body-Text, einem Attribut oder einem Inline-Script-Block landet, und wendet automatisch die passende Kodierfunktion an. Entwickler müssen sich in diesen Systemen erst aktiv gegen den Schutz entscheiden, etwa mit einem |raw-Filter, um ungeschützte Ausgabe zu erzeugen.
Reines PHP in .phtml-Dateien kennt kein automatisches Escaping, weil <?= ?> lediglich Text ausgibt, ohne den umgebenden Kontext zu analysieren. Klassische Magento-Themes vor Hyvä haben genau deshalb häufig komplett auf Escaping verzichtet und sich auf Block-Methoden verlassen, die selten konsequent escaped haben. Hyvä macht die manuelle, aber explizite Kontextwahl zur Pflicht und erzwingt sie über Coding-Standard-Sniffs, die unescapte Ausgaben bereits im Code-Review markieren.
Auto-Escaping ist kein Freifahrtschein: Auch kontextsensitive Engines erkennen nicht jede Verschachtelung zuverlässig, etwa eine Variable innerhalb eines Inline-Event-Handler-Attributs, das selbst wieder JavaScript enthält. Wer sich blind auf die Engine verlässt, statt den Zielkontext bewusst zu prüfen, überträgt das Risiko lediglich von PHP auf die Template-Sprache.
5. Die Magento/Hyvä Escaper-Klasse im Detail
Magento stellt mit Magento\Framework\Escaper eine zentrale Klasse bereit, die in Hyvä-Templates als $escaper injiziert wird und für jeden Kontext eine eigene Methode anbietet. escapeHtml() kodiert für HTML-Body-Text und akzeptiert optional eine Liste erlaubter Tags, wenn begrenztes Markup wie <strong> oder <em> durchgelassen werden soll. escapeHtmlAttr() kodiert zusätzlich Anführungszeichen und ist für jeden Attributwert Pflicht, egal ob href, alt, title oder ein data-Attribut.
escapeJs() kodiert für einfache JavaScript-String-Kontexte, etwa innerhalb eines Inline-Event-Handler-Attributs wie onclick. Für strukturierte Daten wie Arrays oder Objekte ist json_encode() mit den Flags JSON_HEX_TAG, JSON_HEX_APOS, JSON_HEX_QUOT und JSON_HEX_AMP die robustere Wahl, weil escapeJs() keine gültige JSON-Syntax garantiert. escapeUrl() prüft das Schema eines Links und kodiert reservierte Zeichen, escapeXssInUrl() geht einen Schritt weiter und entfernt gezielt XSS-Vektoren wie javascript: aus nutzergesteuerten Redirect-Zielen. escapeCss() schließlich kodiert Werte, die in style-Attribute oder CSS-Blöcke eingebettet werden.
Die Regel für die Auswahl ist einfach: erst den Zielkontext bestimmen, dann die passende Methode wählen. Ein Produktname in der Überschrift braucht escapeHtml(), derselbe Name im alt-Attribut escapeHtmlAttr(), dieselbe SKU in einem onclick-Handler escapeJs(). Das folgende Beispiel zeigt alle sechs Methoden in einem realistischen Produkt-Template.
<?php
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Magento\Catalog\Block\Product\View $block */
$product = $block->getProduct();
?>
<!-- HTML body context: product name rendered as text -->
<h1 class="text-2xl font-bold"><?= $escaper->escapeHtml($product->getName()) ?></h1>
<!-- HTML attribute context: alt/title need quote-safe encoding -->
<img src="<?= $escaper->escapeUrl($block->getImageUrl()) ?>"
alt="<?= $escaper->escapeHtmlAttr($product->getName()) ?>"
title="<?= $escaper->escapeHtmlAttr($product->getMetaTitle()) ?>">
<!-- JS string context inside an inline event handler attribute -->
<button
type="button"
onclick="trackProductView('<?= $escaper->escapeJs($product->getSku()) ?>')"
class="rounded-lg bg-purple-700 px-4 py-2 text-white">
Track view
</button>
<!-- Structured data passed to Alpine: json_encode + hex flags, not escapeHtml -->
<div x-data='<?= /* @noEscape */ json_encode([
'id' => $product->getId(),
'sku' => $product->getSku(),
], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>'>
</div>
<!-- URL context: href is scheme-checked, not just percent-encoded -->
<a href="<?= $escaper->escapeUrl($block->getBackUrl()) ?>">Back</a>
<!-- User-controlled redirect target: strip javascript: and similar vectors -->
<a href="<?= $escaper->escapeXssInUrl($block->getReturnUrl()) ?>">Continue</a>
<!-- CSS context: dynamic value inside an inline style attribute -->
<div style="background-image: url('<?= $escaper->escapeCss($block->getBannerUrl()) ?>');"></div>
6. Doppelt-Encoding: Wenn Escaping zu oft angewendet wird
Doppelt-Encoding entsteht, wenn ein Wert bereits an einer Stelle escaped wurde und an einer zweiten Stelle erneut durch dieselbe oder eine andere Escaping-Funktion läuft. Aus & wird dann &amp;, aus ' wird &#039;, sichtbar für jeden Nutzer als kaputte Sonderzeichen auf der Seite. Häufigste Ursache: Ein ViewModel oder eine Block-Methode escaped den Wert bereits „zur Sicherheit“, und das Template escaped beim Ausgeben erneut, weil niemand mehr eindeutig weiß, ob der Wert schon sicher ist.
Das Problem verschärft sich in URL-Kontexten, wo aus %26 durch erneutes Encoding %2526 wird, was Redirect-Vergleiche, Signatur-Prüfungen oder OAuth-Callbacks stillschweigend zum Scheitern bringt, ohne dass eine offensichtliche Fehlermeldung erscheint. Die zuverlässige Regel lautet: Rohdaten werden in der Domain-Schicht gespeichert und weitergereicht, escaped wird ausschließlich an der letzten Stelle vor der Ausgabe, dort, wo der Zielkontext eindeutig feststeht.
In der Praxis bedeutet das: ViewModel- und Repository-Methoden geben grundsätzlich unescapte Werte zurück, und nur das .phtml-Template, das den konkreten HTML-, Attribut- oder JS-Kontext kennt, ruft die passende Escaper-Methode auf. Diese klare Verantwortungstrennung verhindert fehlendes und doppeltes Escaping gleichermaßen.
<?php
declare(strict_types=1);
// WRONG: escaping happens twice - once in the ViewModel, once in the template.
// Step 1: ViewModel already HTML-escapes the value.
final class ProductViewModel
{
public function getFormattedName(): string
{
return htmlspecialchars($this->product->getName(), ENT_QUOTES, 'UTF-8');
}
}
// Step 2: the phtml template escapes the (already escaped) value again:
// <h1><?= $escaper->escapeHtml($viewModel->getFormattedName()) ?></h1>
//
// Input: M&M's Chocolate
// Step 1: M&M's Chocolate
// Step 2: M&amp;M&#039;s Chocolate <- visibly broken on the page
// RIGHT: escape exactly once, as late as possible, at the final output boundary.
final class ProductViewModel
{
// Return raw domain data - no escaping in the ViewModel layer.
public function getName(): string
{
return $this->product->getName();
}
}
// The template is the only place that knows the destination context:
// <h1><?= $escaper->escapeHtml($viewModel->getName()) ?></h1>
7. DOM-based XSS: innerHTML vs. textContent
Server-seitiges Output-Encoding schützt nicht vor DOM-based XSS, weil dieser Angriffstyp vollständig im Browser abläuft, ohne dass der schädliche Wert jemals den Server durchläuft. Wird ein Wert per innerHTML oder insertAdjacentHTML() in den DOM eingefügt, parst der Browser ihn als HTML-Markup, inklusive eingebetteter Fehler-Handler-Attribute wie onerror, die sofort ausgeführt werden.
Die sichere Alternative ist textContent (oder innerText), weil beide Eigenschaften den Wert ausschließlich als Text setzen, unabhängig davon, welche Zeichen er enthält, ganz ohne HTML-Parsing. Muss tatsächlich HTML-Markup dynamisch eingefügt werden, führt kein Weg an einer echten Sanitizing-Bibliothek wie DOMPurify vorbei, reines Escaping reicht dafür nicht aus, weil erlaubtes Markup ja gerade nicht kodiert werden soll.
In Hyvä-Templates gilt dieselbe Regel für Alpine.js: x-text ist das sichere Äquivalent zu textContent und sollte der Standardfall sein. x-html verhält sich wie innerHTML und darf ausschließlich für vollständig statische, im Code hinterlegte Inhalte verwendet werden, niemals für Werte, die aus Nutzereingaben, Query-Parametern oder externen APIs stammen.
// WRONG: DOM-based XSS - innerHTML parses the string as HTML
function renderSearchSuggestion(term) {
const el = document.querySelector('#suggestion');
el.innerHTML = term; // if term contains an img tag with an onerror handler, it executes
}
// WRONG: insertAdjacentHTML has the exact same problem
function appendNotice(message) {
document.querySelector('#notices')
.insertAdjacentHTML('beforeend', '<div class="notice">' + message + '</div>');
}
// RIGHT: textContent never parses markup, it is always safe for plain text
function renderSearchSuggestion(term) {
const el = document.querySelector('#suggestion');
el.textContent = term;
}
// RIGHT: build elements via the DOM API instead of concatenating HTML strings
function appendNotice(message) {
const div = document.createElement('div');
div.className = 'notice';
div.textContent = message;
document.querySelector('#notices').appendChild(div);
}
// Alpine.js: x-text is the safe equivalent of textContent, x-html behaves like innerHTML
// <div x-text="suggestion"></div> -- safe
// <div x-html="suggestion"></div> -- only for fully trusted, static markup
8. Content-Security-Policy als zusätzliche Verteidigungslinie
Eine Content-Security-Policy (CSP) schränkt ein, welche Skript-Quellen ein Browser überhaupt ausführen darf, und blockiert Inline-Skripte standardmäßig, sofern kein passendes Nonce-Attribut vorhanden ist. Damit fängt CSP genau die Fälle ab, in denen Output-Encoding versagt hat: Selbst wenn ein Angreifer erfolgreich unescapten Code injiziert, verhindert eine strikte Policy ohne 'unsafe-inline', dass der Browser ihn tatsächlich ausführt.
Entscheidend ist die Reihenfolge der Verteidigung: CSP ist eine zusätzliche Schicht oberhalb von korrektem Output-Encoding, niemals ein Ersatz dafür. Eine Policy mit 'unsafe-inline' oder mit Wildcard-Hosts in script-src bietet praktisch keinen Schutz mehr und täuscht nur Sicherheit vor. Magentos Hyvä-CSP-Modul generiert pro Request automatisch ein Nonce, das über $hyvaCsp->registerInlineScript() nach jedem Inline-Script-Block registriert werden muss, sonst wird der Block vom Browser stillschweigend blockiert.
Wer CSP korrekt einsetzt, gewinnt eine Verteidigungslinie, die auch dann noch greift, wenn irgendwo im Code ein Escaping-Fehler übersehen wurde, etwa in einer neu hinzugefügten Drittanbieter-Integration. Ersetzen kann und soll CSP das eigentliche Output-Encoding aber nicht, weil sie serverseitige Logikfehler wie SQL-Injection oder fehlerhafte Redirects gar nicht adressiert.
<?xml version="1.0"?>
<!-- etc/csp_whitelist.xml: explicit allow-list instead of 'unsafe-inline' -->
<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd">
<policies>
<policy id="script-src">
<values>
<value id="mironsoft-analytics" type="host">https://analytics.mironsoft.de</value>
</values>
</policy>
<policy id="style-src">
<values>
<value id="google-fonts" type="host">https://fonts.googleapis.com</value>
</values>
</policy>
</policies>
</csp_whitelist>
<!-- No 'unsafe-inline' in script-src: every inline block needs a per-request nonce. -->
<!-- In the phtml, call $hyvaCsp->registerInlineScript() right after each inline block -->
<!-- so the framework can inject the matching nonce attribute automatically. -->
9. Escaping in der Praxis: Code-Review-Checkliste
Für ein systematisches Code-Review lohnt sich eine feste Reihenfolge: Zuerst jeden Ausgabepunkt identifizieren und den exakten Zielkontext bestimmen, HTML-Body, Attribut, JS-String, URL oder CSS. Danach prüfen, ob die dafür vorgesehene Escaper-Methode verwendet wird und nicht eine generische Funktion, die zufällig für einen anderen Kontext gedacht war.
Anschließend auf Doppelt-Encoding kontrollieren, insbesondere an Schnittstellen zwischen ViewModel und Template, und alle DOM-Manipulationen im JavaScript auf innerHTML- oder insertAdjacentHTML()-Aufrufe durchsuchen, die durch textContent oder x-text ersetzbar wären. Magentos Coding-Standard-Sniff für Templates (Magento2.Security.XssTemplate) findet viele dieser Fälle bereits statisch, bevor der Code überhaupt gemerged wird.
Am Ende hilft ein kurzer Payload-Test mehr als jede Theorie: Werte wie <script>alert(1)</script>, "onmouseover="alert(1) oder javascript:alert(1) in jeden identifizierten Kontext einsetzen und prüfen, ob der Payload im gerenderten HTML als Text erscheint oder tatsächlich ausgeführt wird. Diese Prüfung dauert Minuten und deckt zuverlässig auf, wo eine falsche oder fehlende Kontext-Zuordnung übersehen wurde.
| Ausgabekontext | Unsicher / falscher Kontext | Korrekte Hyvä-Escaper-Methode | Risiko bei falscher Anwendung |
|---|---|---|---|
| HTML-Body-Text | echo $value ohne Escaping |
$escaper->escapeHtml($value) |
Stored/Reflected XSS über Inline-Skripte |
| HTML-Attribut | htmlspecialchars($value) ohne ENT_QUOTES |
$escaper->escapeHtmlAttr($value) |
Attribut-Escape via einfachem Anführungszeichen |
| JavaScript-String | HTML-Escaping im Inline-Script-Block | $escaper->escapeJs($value) / json_encode() |
Script-Injection durch Quotes oder Backslash |
| URL / href | rawurlencode() ohne Schema-Prüfung |
$escaper->escapeUrl() / escapeXssInUrl() |
javascript:-Pseudo-Protokoll als XSS-Vektor |
| CSS / style | direkte Werteinbettung in style="" |
$escaper->escapeCss($value) |
CSS-Injection und Datenexfiltration via url() |
Mironsoft
Security-Audits, XSS-Prävention und Hyvä-Härtung für Magento-Shops
Sind eure Templates gegen XSS wirklich abgesichert?
Wir prüfen eure Magento- und Hyvä-Templates auf fehlendes, falsches und doppeltes Escaping, härten CSP-Konfigurationen und schließen DOM-based-XSS-Lücken in Alpine.js-Komponenten, bevor ein Angreifer sie findet.
Escaping-Audit
Statische Analyse und manuelle Payload-Tests je Ausgabekontext
Escaper-Refactoring
escapeHtml, escapeHtmlAttr, escapeJs und escapeUrl konsequent nachrüsten
CSP-Härtung
Nonce-basierte Content-Security-Policy ohne unsafe-inline aufsetzen
10. Zusammenfassung
Der Kern von Output-Encoding ist die Erkenntnis, dass Escaping kein universeller Schalter ist, sondern für jeden Ausgabekontext eine eigene Regel braucht. HTML-Body-Text, HTML-Attribute, JavaScript-Strings, URLs und CSS-Werte haben jeweils andere gefährliche Zeichen und damit andere korrekte Kodierfunktionen. htmlspecialchars() ohne ENT_QUOTES, ohne explizites Charset oder im falschen Kontext eingesetzt hinterlässt genau die Lücken, die ein Angreifer sucht.
Die Magento/Hyvä-Escaper-Klasse bildet diese Kontexte mit sechs klar benannten Methoden ab: escapeHtml(), escapeHtmlAttr(), escapeJs(), escapeUrl(), escapeCss() und escapeXssInUrl(). Wer konsequent erst den Kontext bestimmt und dann die passende Methode wählt, vermeidet sowohl fehlendes als auch doppeltes Escaping. DOM-based XSS über innerHTML lässt sich zuverlässig durch textContent oder Alpines x-text verhindern, und eine nonce-basierte Content-Security-Policy ergänzt korrektes Encoding als letzte, aber niemals als einzige Verteidigungslinie.
Output-Encoding und Escaping: Das Wichtigste auf einen Blick
Kontext zuerst
HTML-Body, Attribut, JS-String, URL oder CSS bestimmen, bevor überhaupt eine Escaping-Funktion gewählt wird.
htmlspecialchars() richtig nutzen
Immer ENT_QUOTES | ENT_SUBSTITUTE und 'UTF-8' explizit angeben, niemals auf Standardwerte verlassen.
Hyvä Escaper nutzen
escapeHtml, escapeHtmlAttr, escapeJs, escapeUrl, escapeCss und escapeXssInUrl je nach Zielkontext einsetzen.
CSP als zweite Verteidigungslinie
Nonce-basierte Content-Security-Policy ergänzt, ersetzt aber niemals korrektes Output-Encoding.