Safeguard
Application Security•

How do you stop XSS in a Kotlin web app (Spring, Ktor)?

Kotlin null safety does not prevent XSS. Keep escaping on in Thymeleaf and HTML builders, avoid unescaped output, encode by context, and add a CSP.

Safeguard Security Team
4 min read

Kotlin's null safety does nothing for cross-site scripting. To stop XSS in a Kotlin web app, render through a view layer that escapes by default, find every place that opts out of escaping, encode for the context the value lands in, and add a content security policy so a missed escape is harder to exploit.

Where does XSS appear in Kotlin code?

Kotlin web code usually renders in one of three ways: server-side templates such as Thymeleaf, a typesafe HTML DSL such as kotlinx.html, or a JSON API consumed by a JavaScript front end. XSS shows up at the edges of each. A template that outputs unescaped text, a builder call that injects a raw string, or a front end that assigns API data to innerHTML.

A constructed example: a Spring controller passes a user's display name to a profile template. The template author needs bold text in a bio field and switches that one element to unescaped output. Anyone who edits their bio can now ship markup to every visitor of the page.

What does Thymeleaf escape by default?

The Thymeleaf documentation says th:text escapes its value, and that this is the default behaviour. To output markup, you use th:utext, described as "unescaped text". The docs add that th:utext is subject to stricter expression evaluation restrictions for security reasons.

That gives a clean review rule: search templates for th:utext, and for the unescaped inline form of expressions, and require a reason for each. The value should be a constant, or the output of an HTML sanitiser with an allow list, never a raw request or database field.

What about the Kotlin HTML DSL?

The Kotlin documentation describes kotlinx.html as a library that builds DOM elements with statically typed builders. Typed builders are safer than string concatenation, because text content goes through the builder rather than into a hand-built string. I could not confirm from the pages fetched here exactly how every raw-output function behaves, so check the library source or docs for your version. Look for any function that accepts a raw string and treat it like th:utext.

The wider rule holds in any builder: never assemble tags with string templates. A Kotlin string template like a quoted line with a dollar sign variable inside markup bypasses every escaping layer the framework provides.

Which contexts need different encoding?

OWASP's XSS Prevention Cheat Sheet separates contexts. HTML body: entity-encode. Attribute: always quote and encode. JavaScript: place a value only inside a quoted string with a script-aware encoder. URL: percent-encode parameters and validate the scheme of any full URL placed in an href. CSS: values only, in a property position.

OWASP also lists locations to avoid entirely, such as directly inside script or style blocks and inside whole event handler attributes. If your template does that, restructure it. A common fix is to place data in a data attribute, quoted and encoded, and have a static script read it.

What if the front end consumes a Kotlin JSON API?

Serve JSON with an application/json content type, as OWASP advises. On the client, use text-only sinks such as textContent. Use innerHTML only with sanitised output.

How does a CSP help?

MDN explains that a CSP can allow only scripts with a matching nonce or hash, block inline handlers and javascript: URLs, and disable eval. It also says a CSP is not a substitute for sanitising input. Start with the report-only header so you can see violations before enforcing. In Spring, add the header in a filter or the security configuration, and generate a nonce per response.

What to do

  1. List every template and builder call that outputs unescaped content. Require a justification for each.
  2. Ban string-template HTML construction in controllers and services.
  3. Sanitise any user-supplied HTML with an allow list before output.
  4. Use quoted attributes, validated URL schemes, and no untrusted data inside script or style blocks.
  5. Return JSON with the right content type and use text sinks in the browser.
  6. Deploy a nonce-based CSP in report-only mode, then enforce.
  7. Scan dependencies, including front-end packages. The free scan at gold.safeguard.sh/scan covers up to 100 dependencies.

Quick check

Is th:utext always a vulnerability?

No, but it disables escaping. It is safe only for constants or sanitised output, so each use needs a stated reason.

Does Kotlin null safety reduce XSS risk?

No. XSS depends on how text is placed in a page, which the type system does not model.

Sources

Last reviewed: 18 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.