FlawOpen

flawopen.com/xss/Java

● CWE-79 · Alta
Pesquisa · FlawOpen

XSS em Java: proteja Spring MVC e Thymeleaf

th:utext e saída JSP sem escape podem executar JavaScript injetado. Codifique por contexto e sanitize HTML rico com lista permitida (CWE-79).

💡 Explicação em Linguagem Simples (ELI5)

O placar de um estádio tem um campo de nome simples e um modo separado para exibição formatada. th:text do Thymeleaf trata o nome enviado como texto literal; th:utext diz ao renderizador que trate o valor como instruções HTML para o placar. O navegador recebe o text/html do site do estádio e renderiza a marcação com a autoridade do site. Mantenha texto do usuário na saída escapada e sanitize antes de usar um slot sem escape quando a formatação rica for necessária.

Conceitos Centrais e Termos

Thymeleaf th:text vs th:utext
th:text escapa o valor como HTML; th:utext (texto sem escape) o insere como marcação. Na forma inline do Thymeleaf, [[${x}]] escapa e [(${x})] não. Um th:utext ligado a entrada do usuário é XSS, a menos que o valor tenha sido sanitizado antes.
JSP ${} vs <c:out>
No texto de template JSP, ${param.q} é escrito sem escape. <c:out value="${param.q}"/> ou ${fn:escapeXml(param.q)} o escapam. Scriptlets como <%= request.getParameter("q") %> nunca escapam.
Encode.forHtml / forJavaScript
O OWASP Java Encoder tem um método por contexto de saída: Encode.forHtml para conteúdo de elemento, forHtmlAttribute para atributos entre aspas, forJavaScript para literais de string JavaScript e forUriComponent para parâmetros de URL. Codificar como HTML um valor posto dentro de um script não basta.
OWASP Java HTML Sanitizer
Para HTML enviado pelo usuário, uma política como Sanitizers.FORMATTING.and(Sanitizers.LINKS) mantém uma allowlist de tags e atributos e remove scripts, manipuladores de eventos e URLs javascript:. Codificar mostraria as tags como texto; sanitizar mantém as seguras funcionando.
@ResponseBody returning text/html
Um método de @RestController que devolve String com produces = text/html é escrito na resposta como está: nenhum template engine roda, então nada é escapado. Prefira um template de view, ou codifique cada valor concatenado.

Mapeamentos de frameworks

Fluxo de Ataque Passo a Paso

Passo 1

A entrada chega num parâmetro da requisição

O Spring liga q, user ou bio da query string ou do corpo do formulário a um argumento String. A vítima pode receber um link preparado, ou uma bio enviada aparece para outros usuários.

Passo 2

A concatenação monta a resposta

O controller cola o valor no HTML ou num bloco <script>. Com o valor de teste inofensivo <img src=x onerror=alert(1)>, a string devolvida contém uma tag real; com ';alert(1);//, o bloco de script ganha um novo comando.

Passo 3

O navegador a interpreta

Como o método declara text/html, o navegador renderiza o corpo como página. A imagem falha e dispara onerror, ou o bloco de script executa o comando injetado.

Passo 4

O script roda como se fosse o site

O código executa com a origem da aplicação: pode ler o DOM, chamar os endpoints da aplicação com o cookie de sessão da vítima e mudar o que o usuário vê.

Código-Fonte: Vulnerável vs. Seguro

IMPLEMENTAÇÃO VULNERÁVEL
// ProfileController.java: cada endpoint concatena dados da requisição no HTML
@RestController
public class ProfileController {

    // contexto HTML: o valor da query entra como marcação
    @GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
    public String search(@RequestParam String q) {
        return "<h1>Results for " + q + "</h1>";
    }

    // contexto JavaScript: uma aspa em user encerra o literal de string
    @GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
    public String widget(@RequestParam String user) {
        return "<script>const user = '" + user + "';</script>";
    }

    // o texto rico volta com todas as tags e manipuladores intactos
    @PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
    public String preview(@RequestParam String bio) {
        return "<div class=\"bio\">" + bio + "</div>";
    }
}
PATCH SEGURO E ROBUSTO
// ProfileController.java: cada valor é codificado para seu contexto ou sanitizado
@RestController
public class ProfileController {

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

    // forHtml codifica < > & " ' para conteúdo de elemento
    @GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
    public String search(@RequestParam String q) {
        return "<h1>Results for " + Encode.forHtml(q) + "</h1>";
    }

    // forJavaScript escapa aspas, barras invertidas e sequências </script>
    @GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
    public String widget(@RequestParam String user) {
        return "<script>const user = '" + Encode.forJavaScript(user) + "';</script>";
    }

    // o sanitizador mantém formatação e links e descarta o resto
    @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 Verificação de Segurança para Engenharia

Fontes