FlawOpen

flawopen.com/xss/Java

● CWE-79 · Alta
Investigación · FlawOpen

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).

💡 Explicación en Lenguaje Sencillo (ELI5)

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:utext
th:text escapa 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. Un th:utext ligado 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.forHtml para contenido de elementos, forHtmlAttribute para atributos entre comillas, forJavaScript para literales de cadena JavaScript y forUriComponent para 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 URLs javascript:. Codificar mostraría las etiquetas como texto; sanitizar mantiene funcionando las seguras.
@ResponseBody returning text/html
Un método de @RestController que devuelve un String con produces = text/html se 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

Flujo de Ataque Paso a Paso

Paso 1

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.

Paso 2

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.

Paso 3

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.

Paso 4

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

IMPLEMENTACIÓN VULNERABLE
// 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>";
    }
}
PARCHE SEGURO Y ROBUSTO
// 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

Fuentes