FlawOpen

flawopen.com/xss/Java

● CWE-79 · Haute
Recherche · FlawOpen

XSS en Java : sécuriser Spring MVC et Thymeleaf

th:utext et JSP sans échappement peuvent exécuter du JavaScript. Encodez par contexte et assainissez le HTML avec une liste autorisée (CWE-79).

💡 Explication en Langage Simple (ELI5)

Le tableau d’affichage d’un stade propose un champ de nom en texte brut et un mode d’affichage enrichi distinct. th:text de Thymeleaf traite le nom soumis comme du texte littéral ; th:utext demande au moteur de traiter la valeur comme des instructions HTML pour le tableau. Le navigateur reçoit le text/html du site du stade et affiche ce balisage avec l’autorité du site. Gardez le texte utilisateur dans la sortie échappée et assainissez-le avant d’utiliser un emplacement non échappé si le format enrichi est nécessaire.

Concepts Clés et Termes

Thymeleaf th:text vs th:utext
th:text échappe sa valeur en HTML ; th:utext (texte non échappé) l'insère comme balisage. Dans la forme en ligne de Thymeleaf, [[${x}]] échappe et [(${x})] non. Un th:utext lié à une saisie est une XSS, sauf si la valeur a été assainie au préalable.
JSP ${} vs <c:out>
Dans le texte d'un gabarit JSP, ${param.q} est écrit sans échappement. <c:out value="${param.q}"/> ou ${fn:escapeXml(param.q)} l'échappent. Les scriptlets comme <%= request.getParameter("q") %> n'échappent jamais.
Encode.forHtml / forJavaScript
OWASP Java Encoder propose une méthode par contexte de sortie : Encode.forHtml pour le contenu d'élément, forHtmlAttribute pour les attributs entre guillemets, forJavaScript pour les littéraux de chaîne JavaScript et forUriComponent pour les paramètres d'URL. Encoder en HTML une valeur placée dans un script ne suffit pas.
OWASP Java HTML Sanitizer
Pour du HTML fourni par l'utilisateur, une politique comme Sanitizers.FORMATTING.and(Sanitizers.LINKS) applique une liste autorisée de balises et d'attributs et retire scripts, gestionnaires d'événements et URL javascript:. Encoder afficherait les balises comme du texte ; assainir garde les balises sûres fonctionnelles.
@ResponseBody returning text/html
Une méthode de @RestController qui renvoie une String avec produces = text/html est écrite telle quelle dans la réponse : aucun moteur de gabarits ne s'exécute, donc rien n'est échappé. Préférez un gabarit de vue, ou encodez chaque valeur concaténée.

Correspondances de référentiels

Déroulement de l'Attaque Étape par Étape

Étape 1

La saisie arrive dans un paramètre de requête

Spring lie q, user ou bio, venant de la query string ou du corps de formulaire, à un argument String. On peut envoyer un lien piégé à la victime, ou une bio soumise est affichée aux autres utilisateurs.

Étape 2

La concaténation construit la réponse

Le contrôleur colle la valeur dans le HTML ou dans un bloc <script>. Avec la valeur de test inoffensive <img src=x onerror=alert(1)>, la chaîne renvoyée contient une vraie balise ; avec ';alert(1);//, le bloc de script gagne une nouvelle instruction.

Étape 3

Le navigateur l'interprète

Comme la méthode déclare text/html, le navigateur rend le corps comme une page. L'image échoue et déclenche onerror, ou le bloc de script exécute l'instruction injectée.

Étape 4

Le script s'exécute au nom du site

Le code tourne avec l'origine de l'application : il peut lire le DOM, appeler les endpoints de l'application avec le cookie de session de la victime et modifier ce que voit l'utilisateur.

Code Source : Vulnérable vs Sécurisé

IMPLÉMENTATION VULNÉRABLE
// ProfileController.java : chaque endpoint concatène des données HTTP dans le HTML
@RestController
public class ProfileController {

    // contexte HTML : la valeur de la query est insérée comme balisage
    @GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
    public String search(@RequestParam String q) {
        return "<h1>Results for " + q + "</h1>";
    }

    // contexte JavaScript : une apostrophe dans user termine le littéral
    @GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
    public String widget(@RequestParam String user) {
        return "<script>const user = '" + user + "';</script>";
    }

    // le texte riche est renvoyé avec toutes ses balises et gestionnaires
    @PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
    public String preview(@RequestParam String bio) {
        return "<div class=\"bio\">" + bio + "</div>";
    }
}
PATCH SÉCURISÉ ET ROBUSTE
// ProfileController.java : chaque valeur est encodée pour son contexte ou assainie
@RestController
public class ProfileController {

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

    // forHtml encode < > & " ' pour le contenu d'élément
    @GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
    public String search(@RequestParam String q) {
        return "<h1>Results for " + Encode.forHtml(q) + "</h1>";
    }

    // forJavaScript échappe guillemets, antislashs et séquences </script>
    @GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
    public String widget(@RequestParam String user) {
        return "<script>const user = '" + Encode.forJavaScript(user) + "';</script>";
    }

    // l'assainisseur garde la mise en forme et les liens, et retire tout le reste
    @PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
    public String preview(@RequestParam String bio) {
        return "<div class=\"bio\">" + BIO_POLICY.sanitize(bio) + "</div>";
    }
}

Liste de Contrôle de Sécurité pour l'Ingénierie

Sources