flawopen.com/xss/Java
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).
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:utextth:textescapa 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. Umth:utextligado 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.forHtmlpara conteúdo de elemento,forHtmlAttributepara atributos entre aspas,forJavaScriptpara literais de string JavaScript eforUriComponentpara 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 URLsjavascript:. Codificar mostraria as tags como texto; sanitizar mantém as seguras funcionando. @ResponseBody returning text/html- Um método de
@RestControllerque devolveStringcomproduces = 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
- Referência CWE para a fraqueza descrita neste guia: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Fluxo de Ataque Passo a Paso
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.
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.
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.
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
// 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>";
}
}
// 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
- ✓Ative o Find Security Bugs no SpotBugs e faça o build falhar em
XSS_SERVLET,XSS_JSP_PRINTeXSS_REQUEST_PARAMETER_TO_SERVLET_WRITER. - ✓Procure na CI por
th:utext,[(,<%=e${...}soltos em JSP nos templates; cada ocorrência deve renderizar conteúdo sanitizado ou constante. - ✓Codifique para o contexto exato com o OWASP Java Encoder (
forHtml,forHtmlAttribute,forJavaScript,forUriComponent) sempre que um controller montar marcação por conta própria. - ✓Passe HTML enviado por usuários por uma política do OWASP Java HTML Sanitizer antes que chegue a
th:utextou ao corpo de uma resposta. - ✓Configure o
headers().contentSecurityPolicy(...)do Spring Security comscript-srcbaseado em nonce e mantenha o cookie de sessãoHttpOnlyeSameSite=Lax.