flawopen.com/xss/Java
XSS в Java: защита Spring MVC и Thymeleaf
th:utext и JSP без экранирования могут выполнить JavaScript. Кодируйте по контексту и очищайте HTML по списку разрешений (CWE-79).
На табло стадиона есть обычное поле имени и отдельный режим форматированного вывода. th:text в Thymeleaf показывает отправленное имя как обычный текст; th:utext просит рендерер трактовать значение как HTML-инструкции для табло. Браузер получает text/html сайта стадиона и отображает разметку с полномочиями сайта. Оставляйте пользовательский ввод в экранируемом выводе и очищайте его перед неэкранированным слотом шаблона, если нужно форматирование.
Ключевые понятия и термины
Thymeleaf th:text vs th:utextth:textэкранирует значение как HTML;th:utext(неэкранированный текст) вставляет его как разметку. Во встроенной форме Thymeleaf[[${x}]]экранирует, а[(${x})]— нет.th:utext, связанный с пользовательским вводом, — это XSS, если значение предварительно не очищено.JSP ${} vs <c:out>- В тексте шаблона JSP
${param.q}выводится без экранирования.<c:out value="${param.q}"/>или${fn:escapeXml(param.q)}его экранируют. Скриптлеты вроде<%= request.getParameter("q") %>не экранируют никогда. Encode.forHtml / forJavaScript- У OWASP Java Encoder по методу на каждый контекст вывода:
Encode.forHtmlдля содержимого элемента,forHtmlAttributeдля атрибутов в кавычках,forJavaScriptдля строковых литералов JavaScript иforUriComponentдля параметров URL. HTML-кодирования значения внутри скрипта недостаточно. OWASP Java HTML Sanitizer- Для HTML от пользователя политика вроде
Sanitizers.FORMATTING.and(Sanitizers.LINKS)держит белый список тегов и атрибутов и удаляет скрипты, обработчики событий и URLjavascript:. Кодирование показало бы теги как текст; очистка оставляет безопасные теги рабочими. @ResponseBody returning text/html- Метод
@RestController, возвращающийStringсproduces = text/html, пишется в ответ как есть: никакой шаблонизатор не работает, и ничего не экранируется. Лучше используйте шаблон представления или кодируйте каждое склеиваемое значение.
Связи со стандартами
- Ссылка CWE для слабого места, описанного в этом руководстве: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Пошаговый сценарий атаки
Ввод приходит в параметре запроса
Spring привязывает q, user или bio из query string или тела формы к аргументу String. Жертве можно прислать подготовленную ссылку, либо отправленное bio показывается другим пользователям.
Склейка собирает ответ
Контроллер вклеивает значение в HTML или в блок <script>. С безобидным тестовым значением <img src=x onerror=alert(1)> возвращаемая строка содержит настоящий тег; с ';alert(1);// блок скрипта получает новый оператор.
Браузер её разбирает
Поскольку метод объявляет text/html, браузер отображает тело как страницу. Картинка не загружается и вызывает onerror, или блок скрипта выполняет внедрённый оператор.
Скрипт выполняется от имени сайта
Код работает с источником (origin) приложения: читает DOM, вызывает эндпоинты приложения с cookie сессии жертвы и меняет то, что видит пользователь.
Исходный код: Уязвимый vs Защищённый вариант
// ProfileController.java: каждый эндпоинт вклеивает данные запроса в HTML
@RestController
public class ProfileController {
// контекст HTML: значение из query вставляется как разметка
@GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
public String search(@RequestParam String q) {
return "<h1>Results for " + q + "</h1>";
}
// контекст JavaScript: кавычка в user завершает строковый литерал
@GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
public String widget(@RequestParam String user) {
return "<script>const user = '" + user + "';</script>";
}
// форматированный текст возвращается со всеми тегами и обработчиками
@PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
public String preview(@RequestParam String bio) {
return "<div class=\"bio\">" + bio + "</div>";
}
}
// ProfileController.java: каждое значение кодируется под свой контекст или очищается
@RestController
public class ProfileController {
private static final PolicyFactory BIO_POLICY = Sanitizers.FORMATTING.and(Sanitizers.LINKS);
// forHtml кодирует < > & " ' для содержимого элемента
@GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
public String search(@RequestParam String q) {
return "<h1>Results for " + Encode.forHtml(q) + "</h1>";
}
// forJavaScript экранирует кавычки, обратные слэши и последовательности </script>
@GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
public String widget(@RequestParam String user) {
return "<script>const user = '" + Encode.forJavaScript(user) + "';</script>";
}
// санитайзер сохраняет форматирование и ссылки и отбрасывает всё остальное
@PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
public String preview(@RequestParam String bio) {
return "<div class=\"bio\">" + BIO_POLICY.sanitize(bio) + "</div>";
}
}
Чек-лист по защите системы для инженеров
- ✓Включите Find Security Bugs в SpotBugs и роняйте сборку на
XSS_SERVLET,XSS_JSP_PRINTиXSS_REQUEST_PARAMETER_TO_SERVLET_WRITER. - ✓Ищите в CI в шаблонах
th:utext,[(,<%=и голые${...}в JSP; каждое вхождение должно выводить очищенное или константное содержимое. - ✓Кодируйте под точный контекст с OWASP Java Encoder (
forHtml,forHtmlAttribute,forJavaScript,forUriComponent) всякий раз, когда контроллер сам собирает разметку. - ✓Пропускайте HTML от пользователей через политику OWASP Java HTML Sanitizer до того, как он попадёт в
th:utextили тело ответа. - ✓Настройте
headers().contentSecurityPolicy(...)в Spring Security сscript-srcна основе nonce и держите cookie сессии сHttpOnlyиSameSite=Lax.