flawopen.com/xss/Java
XSS en Java: protege Spring MVC y Thymeleaf
th:utext y JSP sin escape pueden ejecutar JavaScript. Codifica por contexto y sanea HTML con lista permitida (CWE-79).
El marcador de un estadio tiene un campo de nombre simple y otro modo para mostrar contenido enriquecido. th:text de Thymeleaf trata el nombre enviado como texto literal; th:utext indica al renderizador que interprete el valor como instrucciones HTML para el marcador. El navegador recibe el text/html del sitio del estadio y representa ese marcado con la autoridad del sitio. Mantén el texto del usuario en la salida escapada y sanea antes de usar un espacio sin escape si necesitas formato enriquecido.
Conceptos Clave y Términos
Thymeleaf th:text vs th:utextth:textescapa su valor como HTML;th:utext(texto sin escapar) lo inserta como marcado. En la forma en línea de Thymeleaf,[[${x}]]escapa y[(${x})]no. Unth:utextligado a entrada del usuario es XSS salvo que el valor se haya sanitizado antes.JSP ${} vs <c:out>- En el texto de una plantilla JSP,
${param.q}se escribe sin escapar.<c:out value="${param.q}"/>o${fn:escapeXml(param.q)}lo escapan. Los scriptlets como<%= request.getParameter("q") %>nunca escapan. Encode.forHtml / forJavaScript- OWASP Java Encoder tiene un método por contexto de salida:
Encode.forHtmlpara contenido de elementos,forHtmlAttributepara atributos entre comillas,forJavaScriptpara literales de cadena JavaScript yforUriComponentpara parámetros de URL. Codificar como HTML un valor puesto dentro de un script no basta. OWASP Java HTML Sanitizer- Para HTML aportado por el usuario, una política como
Sanitizers.FORMATTING.and(Sanitizers.LINKS)mantiene una lista permitida de etiquetas y atributos y elimina scripts, manejadores de eventos y URLsjavascript:. Codificar mostraría las etiquetas como texto; sanitizar mantiene funcionando las seguras. @ResponseBody returning text/html- Un método de
@RestControllerque devuelve unStringconproduces = text/htmlse escribe en la respuesta tal cual: no se ejecuta ningún motor de plantillas, así que nada se escapa. Prefiere una plantilla de vista o codifica cada valor que concatenes.
Mapeos de marcos
- Referencia CWE para la debilidad descrita en esta guía: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Flujo de Ataque Paso a Paso
La entrada llega en un parámetro de la petición
Spring enlaza q, user o bio de la query string o del cuerpo del formulario a un argumento String. A la víctima se le puede mandar un enlace preparado, o una bio enviada se muestra a otros usuarios.
La concatenación construye la respuesta
El controlador pega el valor en el HTML o en un bloque <script>. Con el valor de prueba inofensivo <img src=x onerror=alert(1)>, la cadena devuelta contiene una etiqueta real; con ';alert(1);//, el bloque de script gana una nueva sentencia.
El navegador la interpreta
Como el método declara text/html, el navegador muestra el cuerpo como una página. La imagen falla y dispara onerror, o el bloque de script ejecuta la sentencia inyectada.
El script se ejecuta como si fuera el sitio
El código se ejecuta con el origen de la aplicación: puede leer el DOM, llamar a los endpoints de la aplicación con la cookie de sesión de la víctima y cambiar lo que ve el usuario.
Código Fuente: Vulnerable vs. Seguro
// ProfileController.java: cada endpoint concatena datos de la petición en el HTML
@RestController
public class ProfileController {
// contexto HTML: el valor de la query entra como marcado
@GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
public String search(@RequestParam String q) {
return "<h1>Results for " + q + "</h1>";
}
// contexto JavaScript: una comilla en user cierra el literal de cadena
@GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
public String widget(@RequestParam String user) {
return "<script>const user = '" + user + "';</script>";
}
// el texto enriquecido vuelve con todas sus etiquetas y manejadores
@PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
public String preview(@RequestParam String bio) {
return "<div class=\"bio\">" + bio + "</div>";
}
}
// ProfileController.java: cada valor se codifica para su contexto o se sanitiza
@RestController
public class ProfileController {
private static final PolicyFactory BIO_POLICY = Sanitizers.FORMATTING.and(Sanitizers.LINKS);
// forHtml codifica < > & " ' para contenido de elementos
@GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
public String search(@RequestParam String q) {
return "<h1>Results for " + Encode.forHtml(q) + "</h1>";
}
// forJavaScript escapa comillas, barras invertidas y secuencias </script>
@GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
public String widget(@RequestParam String user) {
return "<script>const user = '" + Encode.forJavaScript(user) + "';</script>";
}
// el sanitizador conserva formato y enlaces y descarta todo lo demás
@PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
public String preview(@RequestParam String bio) {
return "<div class=\"bio\">" + BIO_POLICY.sanitize(bio) + "</div>";
}
}
Lista de Verificación de Seguridad para Ingeniería
- ✓Activa Find Security Bugs en SpotBugs y haz fallar el build con
XSS_SERVLET,XSS_JSP_PRINTyXSS_REQUEST_PARAMETER_TO_SERVLET_WRITER. - ✓Busca en la CI
th:utext,[(,<%=y${...}sueltos en JSP dentro de las plantillas; cada coincidencia debe mostrar contenido sanitizado o constante. - ✓Codifica para el contexto exacto con OWASP Java Encoder (
forHtml,forHtmlAttribute,forJavaScript,forUriComponent) siempre que un controlador construya marcado por su cuenta. - ✓Pasa el HTML aportado por usuarios por una política de OWASP Java HTML Sanitizer antes de que llegue a
th:utexto al cuerpo de una respuesta. - ✓Configura
headers().contentSecurityPolicy(...)de Spring Security conscript-srcbasado en nonce y mantén la cookie de sesiónHttpOnlyySameSite=Lax.