FlawOpen

flawopen.com/xss/Java

● CWE-79 · Hoch
Sicherheitsforschung · FlawOpen

Java-XSS: Spring MVC und Thymeleaf absichern

th:utext und JSP ohne Escaping können JavaScript ausführen. Escapen Sie kontextbezogen und bereinigen Sie HTML per Allowlist (CWE-79).

💡 Einfache Erklärung (ELI5)

Die Anzeigetafel eines Stadions hat ein einfaches Namensfeld und einen getrennten Modus für formatierte Inhalte. Thymeleafs th:text behandelt einen eingegebenen Namen als wörtlichen Text; th:utext weist den Renderer an, den Wert als HTML-Anweisungen für die Tafel zu behandeln. Der Browser erhält das text/html der Stadionwebsite und zeigt das Markup mit deren Autorität an. Lassen Sie Nutzereingaben im maskierten Ausgabepfad und bereinigen Sie sie vor einem unmaskierten Template-Platzhalter, wenn Formatierung nötig ist.

Kernkonzepte & Begriffe

Thymeleaf th:text vs th:utext
th:text maskiert seinen Wert als HTML; th:utext (unmaskierter Text) fügt ihn als Markup ein. In Thymeleafs Inline-Form maskiert [[${x}]], [(${x})] nicht. Ein an Nutzereingaben gebundenes th:utext ist XSS, sofern der Wert nicht vorher bereinigt wurde.
JSP ${} vs <c:out>
Im JSP-Template-Text wird ${param.q} ohne Maskierung ausgegeben. <c:out value="${param.q}"/> oder ${fn:escapeXml(param.q)} maskieren ihn. Scriptlets wie <%= request.getParameter("q") %> maskieren nie.
Encode.forHtml / forJavaScript
Der OWASP Java Encoder hat eine Methode pro Ausgabekontext: Encode.forHtml für Elementinhalt, forHtmlAttribute für quotierte Attribute, forJavaScript für JavaScript-String-Literale und forUriComponent für URL-Parameter. Einen Wert in einem Skript nur HTML-zu-kodieren, reicht nicht.
OWASP Java HTML Sanitizer
Für nutzergeliefertes HTML hält eine Policy wie Sanitizers.FORMATTING.and(Sanitizers.LINKS) eine Allowlist von Tags und Attributen und entfernt Skripte, Event-Handler und javascript:-URLs. Kodieren würde die Tags als Text zeigen; Bereinigen lässt die sicheren funktionieren.
@ResponseBody returning text/html
Eine @RestController-Methode, die einen String mit produces = text/html zurückgibt, wird unverändert in die Antwort geschrieben: Keine Template-Engine läuft, also wird nichts maskiert. Besser ein View-Template verwenden oder jeden verketteten Wert kodieren.

Framework-Zuordnungen

Schritt-für-Schritt Angriffsablauf

Schritt 1

Die Eingabe kommt als Request-Parameter

Spring bindet q, user oder bio aus Query-String oder Formular-Body an ein String-Argument. Dem Opfer kann ein präparierter Link geschickt werden, oder eine eingereichte Bio wird anderen Nutzern angezeigt.

Schritt 2

Verkettung baut die Antwort

Der Controller klebt den Wert ins HTML oder in einen <script>-Block. Mit dem harmlosen Testwert <img src=x onerror=alert(1)> enthält der zurückgegebene String ein echtes Tag; mit ';alert(1);// bekommt der Skriptblock eine neue Anweisung.

Schritt 3

Der Browser parst sie

Weil die Methode text/html deklariert, rendert der Browser den Body als Seite. Das Bild schlägt fehl und löst onerror aus, oder der Skriptblock führt die eingeschleuste Anweisung aus.

Schritt 4

Das Skript läuft im Namen der Website

Der Code läuft mit dem Origin der Anwendung: Er kann das DOM lesen, die Endpunkte der Anwendung mit dem Session-Cookie des Opfers aufrufen und ändern, was der Nutzer sieht.

Quellcode: Verwundbar vs. Sicher

VERWUNDBARE IMPLEMENTIERUNG
// ProfileController.java: jeder Endpunkt verkettet Request-Daten ins HTML
@RestController
public class ProfileController {

    // HTML-Kontext: der Query-Wert wird als Markup eingefügt
    @GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
    public String search(@RequestParam String q) {
        return "<h1>Results for " + q + "</h1>";
    }

    // JavaScript-Kontext: ein Anführungszeichen in user beendet das String-Literal
    @GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
    public String widget(@RequestParam String user) {
        return "<script>const user = '" + user + "';</script>";
    }

    // Rich Text kommt mit allen Tags und Handlern zurück
    @PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
    public String preview(@RequestParam String bio) {
        return "<div class=\"bio\">" + bio + "</div>";
    }
}
GEHÄRTETER SICHERHEITS-PATCH
// ProfileController.java: jeder Wert wird für seinen Kontext kodiert oder bereinigt
@RestController
public class ProfileController {

    private static final PolicyFactory BIO_POLICY = Sanitizers.FORMATTING.and(Sanitizers.LINKS);

    // forHtml kodiert < > & " ' für Elementinhalt
    @GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
    public String search(@RequestParam String q) {
        return "<h1>Results for " + Encode.forHtml(q) + "</h1>";
    }

    // forJavaScript maskiert Anführungszeichen, Backslashes und </script>-Folgen
    @GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
    public String widget(@RequestParam String user) {
        return "<script>const user = '" + Encode.forJavaScript(user) + "';</script>";
    }

    // der Sanitizer behält Formatierung und Links und verwirft alles andere
    @PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
    public String preview(@RequestParam String bio) {
        return "<div class=\"bio\">" + BIO_POLICY.sanitize(bio) + "</div>";
    }
}

Checkliste für Engineering & Systemsicherheit

Quellen