Safeguard
Application Security•

Does Scala's Twirl protect against XSS in Play apps?

Twirl templates in Play escape output by default, but Html wrapping, string interpolation and unchecked URLs bring XSS back. Here is what to review.

Safeguard Security Team
4 min read

Mostly yes. Twirl, the template engine in Play, escapes dynamic content by default according to the template type, so plain output is encoded. XSS returns when you wrap user data in Html, build markup with string interpolation, or place unchecked values into URLs and scripts. Those are the places to review.

What does Twirl do by default?

The Play documentation says that by default, dynamic content parts in a Twirl template are escaped according to the rules of the template type, such as HTML or XML. A template like this is safe for ordinary text:

@(msg: String)
<h1>@msg</h1>

If msg is a script element, the browser receives the encoded form and shows the text. The developer did nothing special. That is the point of default escaping: the safe path is also the short one.

The same docs describe how to output a raw fragment: wrap it in the template content type, for example @Html(article.content). They add a note that doing this is a common cause of cross site scripting vulnerabilities and is dangerous if the user can control the content.

How does a Scala app typically lose that protection?

Here is a constructed example, a controller that formats a notification banner:

def banner(user: String) = Action {
  Ok(Html(s"<div class='note'>Welcome back, $user</div>"))
}

This looks tidy and it skips the template engine completely. The string interpolator builds markup with the user name inside it, and Html(...) tells Play the string is already safe. The reviewer has to notice that the word "safe" was declared by the author, not verified.

Twirl also offers its own interpolator, written as html"..." after an import, which the documentation presents as a way to compose template fragments. Check how it treats the values you give it before relying on it for user data. The plain s"..." interpolator is a Scala string builder and knows nothing about HTML.

Which other contexts need attention?

HTML escaping suits element content and quoted attributes. OWASP's XSS Prevention Cheat Sheet treats other contexts separately, and these matter in Play apps:

  • URLs. A link built from user input can carry a script scheme. Parse it, check the scheme, and reject anything but the schemes you expect.
  • JavaScript blocks. Do not write server values into inline script. Return JSON from an action and read it in client code that does not execute it.
  • Rich text fields. Use an HTML sanitizer before wrapping the result in Html. A hand written filter will miss cases.
  • JSON and XML endpoints. Set the right content type so a browser does not treat a response as a page.

OWASP also recommends a Content Security Policy as an additional layer and says it should not be the main defense, since it is easy to misconfigure. Set it in one shared place so no action can forget it.

What to do

  1. Search the project for Html(, HtmlFormat.raw and similar wrappers. Each hit needs a written reason.
  2. Search controllers for s"< and other markup built in strings. Move the markup into a template.
  3. Check every template value that lands in an href, src or style attribute.
  4. Run user supplied rich text through a sanitizer first.
  5. Add a CSP from one shared place, and start in report mode.
  6. Write a test that submits a script element and asserts the rendered page shows it as text.
  7. Keep Play and your template and sanitizer libraries updated, and scan the sbt dependency tree.

Safeguard can scan Scala dependency manifests and code in a repository, which helps with steps 1, 2 and 7. The review habits in the other steps stay with your team.

Quick check

Is @Html(value) ever fine?

Yes, for content you control or content that has been sanitized, as long as the use is deliberate and reviewed. For anything a user can influence, it is unsafe.

Does escaping in the template stop XSS in a script block?

Not reliably. HTML escaping is for HTML contexts. Avoid putting server values inside script blocks and pass data as JSON instead.

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.