flawopen.com/xss/Java
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).
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:utextth: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. Unth:utextlié à 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.forHtmlpour le contenu d'élément,forHtmlAttributepour les attributs entre guillemets,forJavaScriptpour les littéraux de chaîne JavaScript etforUriComponentpour 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 URLjavascript:. Encoder afficherait les balises comme du texte ; assainir garde les balises sûres fonctionnelles. @ResponseBody returning text/html- Une méthode de
@RestControllerqui renvoie uneStringavecproduces = text/htmlest é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
- Référence CWE pour la faiblesse décrite dans ce guide : CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Déroulement de l'Attaque Étape par Étape
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.
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.
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.
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é
// 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>";
}
}
// 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
- ✓Activez Find Security Bugs dans SpotBugs et faites échouer le build sur
XSS_SERVLET,XSS_JSP_PRINTetXSS_REQUEST_PARAMETER_TO_SERVLET_WRITER. - ✓Recherchez en CI
th:utext,[(,<%=et les${...}nus en JSP dans les gabarits ; chaque occurrence doit rendre un contenu assaini ou constant. - ✓Encodez pour le contexte exact avec OWASP Java Encoder (
forHtml,forHtmlAttribute,forJavaScript,forUriComponent) chaque fois qu'un contrôleur construit lui-même du balisage. - ✓Faites passer tout HTML fourni par un utilisateur par une politique OWASP Java HTML Sanitizer avant qu'il n'atteigne
th:utextou le corps d'une réponse. - ✓Configurez
headers().contentSecurityPolicy(...)de Spring Security avec unscript-srcà nonce et gardez le cookie de sessionHttpOnlyetSameSite=Lax.