th:text vs th:utext in Thymeleaf: Choosing Between Safe Escaping and Raw HTML
Thymeleaf's th:text escapes HTML while th:utext renders it raw. Learn when each is correct, the XSS risk of getting it wrong, and how to verify your templates.
22 Oct 2025, 23:51 UTC

Your CMS stores article bodies as HTML — paragraphs, links, the occasional tag. You wire up a Thymeleaf template, write th:text="${article.body}", and the page renders <p>Hello</p> as literal text on screen. Swap in th:utext and the markup renders properly. That one-letter difference is the whole decision: escaped text versus raw HTML, and picking wrong in either direction either breaks your page or opens an XSS hole.
What each attribute actually does
th:text evaluates the expression and HTML-escapes the result before writing it into the element's body. Characters like <, >, and & become <, >, and &. The browser displays them as literal characters rather than interpreting them as markup. This is the default you want for anything a user typed.
th:utext ("unescaped text") writes the result verbatim. If the model value contains <strong>important</strong>, the browser receives those tags and renders bold text. Thymeleaf trusts you completely here — it performs no sanitization whatsoever.
A concrete example
Assume a Spring MVC controller puts an article into the model, where body contains trusted HTML from your editorial pipeline and comment contains raw user input:
<article>
<div th:utext="${article.body}">Placeholder body</div>
<p th:text="${comment.text}">Placeholder comment</p>
</article>If article.body is <p>Hi <em>there</em></p>, th:utext renders real paragraphs and emphasis, while th:text would show the tags as text. Conversely, if comment.text is <script>alert(1)</script>, th:text renders it as harmless visible text, while th:utext would execute it in every visitor's browser.
Verification is simple: view the page source. Escaped output shows entities (<script>); raw output shows intact tags. You can also unit-test with Thymeleaf's TemplateEngine directly — feed a model, process the template, and assert on the rendered string. No server needed.
The security trade-off
The rule of thumb: th:text is the default; th:utext requires justification. Ask where the string came from. If any part of it originated from a user, an external API, or an imported feed, raw output is a cross-site scripting (XSS) vector — an attacker can inject scripts that steal session cookies or act as the victim.
If you genuinely need to render user-supplied rich text (say, Markdown comments converted to HTML), sanitize server-side with a library such as OWASP Java HTML Sanitizer or jsoup's safelist cleaner before it reaches the model. th:utext should only ever see the output of that sanitizer, never the raw input.
Natural templating and a workflow caveat
Because browsers ignore unknown attributes, a Thymeleaf template still opens as a static HTML file — the placeholder text inside the element is what a designer sees, and Thymeleaf replaces it at render time. This "natural templating" is a genuine workflow benefit, but it has a sharp edge with th:utext: the placeholder you write for the design view is static and safe, while the runtime value is dynamic. Review both states. A template that looks harmless in the browser preview can still emit dangerous markup once real data flows through it.
Practical checklist
- Default to
th:texteverywhere; treat everyth:utextas a code-review flag. - For each
th:utext, document why the source is trusted (CMS pipeline, sanitized, server-generated). - Sanitize any user-influenced HTML before it enters the model — never rely on the template layer for this.
- Check rendered page source in staging: entities mean escaped, intact tags mean raw.
The decision is cheap to make correctly and expensive to make wrong. When in doubt, escape — a page showing stray <p> text is a cosmetic bug; an XSS vulnerability is a breach.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.