Choose your stack

Node.js · Express

Fix cross-site scripting in Node.js and Express

Print every field through EJS's escaping tag <%= %>, which turns & < > " and ' into entities, and never through the raw tag <%- %>.

Open the fix card
Python · Flask

Fix cross-site scripting in Python and Flask

Let Jinja autoescaping do the work: render {{ value }} with no |safe filter and no Markup() around user input, so & < > " and ' become entities in both text and quoted attributes.

Open the fix card
Java · Spring Boot

Fix cross-site scripting in Java and Spring Boot

Bind text with th:text and attributes with th:attr or th:title, which HTML-escape & < > " and ' — never th:utext for user data.

Open the fix card
C# · ASP.NET Core

Fix cross-site scripting in C# and ASP.NET Core

Let Razor's @ encode the output: @Model.Name and @Model.Bio HTML-encode & < > " ' and more, in text and in a quoted attribute.

Open the fix card
TypeScript · Next.js

Fix XSS in TypeScript and Next.js

Render user text as React children and attributes, which React escapes, and keep it out of dangerouslySetInnerHTML; if a field must carry HTML, sanitize it with DOMPurify first.

Open the fix card
Python · Django

Fix XSS in Python and Django

Let Django's autoescaping work: render {{ value }} without |safe, mark_safe() or {% autoescape off %}, so < > ' " and & become entities in text and quoted attributes.

Open the fix card
Go · net/http

Fix cross-site scripting in Go and net/http

Render HTML with html/template, never text/template: it escapes each value for where it lands: text, attribute or URL.

Open the fix card
Ruby · Rails

Fix XSS in Ruby on Rails

Let ERB escape: <%= @name %> and tag helpers such as image_tag(..., alt: @name) escape & < > " and ', unless the value is html_safe.

Open the fix card
PHP · Laravel

Fix cross-site scripting in PHP and Laravel

Print user data with Blade's {{ $value }}, which runs htmlspecialchars() with ENT_QUOTES, so < > & " and ' become entities in text and quoted attributes.

Open the fix card

Stacks without a card

How it happens

  • The template reaches for the engine's raw output hatch, which prints the value with no encoding: EJS <%- %>, Jinja |safe or Markup(), Thymeleaf th:utext, Razor @Html.Raw. A stored or reflected value then carries <script>, an <img onerror=...> tag or a closing tag that breaks out of the surrounding element.
  • The value sits in a quoted attribute and the quote is not encoded, so an input such as " onmouseover="... closes the attribute and adds an event handler. An unquoted attribute is worse: a space is enough to add one.
  • The value is a URL placed in href or src. HTML-escaping does not help here, because javascript:alert(1) contains no HTML metacharacters, so the escaped string is still a working javascript: URL that runs when the link is clicked.
  • A hand-rolled defence is trusted instead of the engine. Escaping only < and > leaves quotes live for attribute injection; a regex that strips <script> misses <img onerror>, <svg onload> and every other event-handler vector; a javascript: blocklist misses mixed case, embedded tabs or newlines, and other dangerous schemes such as data: and vbscript:.

Fix principles

  • Render every value through the template engine's default, context-aware escaping (EJS <%= %>, Jinja {{ }} with autoescape, Thymeleaf th:text/th:attr, Razor @). Treat the raw hatch as off-limits for anything a user can influence.
  • Always wrap attribute values in quotes so a value cannot grow into a second attribute, and let the engine encode the quote characters.
  • A URL is its own context: before putting it in href or src, parse it the way a browser does and allow only known-safe schemes (http, https, mailto) or a relative URL, and replace anything else. Escaping is not a substitute.
  • For rich text that must keep some HTML, run it through a maintained, allowlist-based HTML sanitizer (DOMPurify, nh3/Ammonia, OWASP Java HTML Sanitizer or jsoup, Ganss.Xss/HtmlSanitizer) and never a regex. React's dangerouslySetInnerHTML and the equivalents in other frameworks need the same treatment.
  • Add a Content-Security-Policy as defence in depth so a missed sink is less likely to execute, but do not let it replace output encoding.
  • Keep framework debug pages off in production: they echo input and internals that help an attacker tune a payload.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
  • The regression tests render real payloads (a <script> body, an <img onerror>, a quote that tries to break out of an attribute, a javascript: URL and one with an embedded tab) and parse the result with an HTML5 parser, asserting no script element, no on* handler attribute and no non-http(s) link survives (the Next.js card accepts the throwing javascript: URL React 19 substitutes for any javascript: href, and adds a JSON-LD breakout test). Functional tests confirm that text with <, &, quotes and non-ASCII characters still displays literally.
  • Static analysis is uneven. Semgrep CE has registry rules for the EJS and Jinja raw escape hatches but none for Thymeleaf th:utext or a bare Razor @Html.Raw. CodeQL models the EJS <%- %> and Razor @Html.Raw sinks but does not read Jinja or Thymeleaf template contents, so a |safe filter or th:utext is invisible to it. CodeQL was not executed for these cards, except the Go card, where neither of its Go XSS queries followed the value through the card’s interface; each card’s scannerRules records what was actually run and what it found.

References

  1. OWASP Cross Site Scripting Prevention Cheat Sheet
  2. OWASP DOM based XSS Prevention Cheat Sheet
  3. OWASP Top 10:2025 A05 Injection
  4. CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
  5. CAPEC-63: Cross-Site Scripting (XSS)
  6. WHATWG URL Standard: URL parsing (scheme, tab and newline removal)
  7. MDN: Content-Security-Policy

Attack patterns:

Practice in the browser

Guides on this weakness

Guide

Prevent XSS at the output boundary

Choose text rendering, context-aware encoding, or deliberate HTML sanitization according to how the browser will interpret the value.

Guide

Configure CORS and CSP around the browser capabilities you need

Control who can read API responses and which resources a page may execute without confusing either control with authorization.

Guide

Render rich text without granting it application privileges

Choose text rendering or a restricted HTML policy, then test the final DOM after every transformation.

Guide

Keep generated output out of executable contexts

Choose safe rendering and narrow operations so model text cannot become browser code, database syntax or an unrestricted command.

Guide

Prevent DOM XSS, postMessage abuse and client-side injection

Trace attacker-controlled values from the URL, messages and storage to the browser APIs that parse HTML or run code, then close those sinks with safe APIs, a sanitizer, Trusted Types and strict message checks.

Guide

Apply CISA Secure by Design: principles, pledge goals and evidence

What CISA's Secure by Design guidance and pledge ask of software manufacturers as of October 2026, and how to turn each of the three principles, the seven pledge goals and the Product Security Bad Practices into engineering actions, metrics and evidence.

Guide

Worked threat model: an LLM agent with tools

A complete worked threat model for an LLM agent with tools: the data-flow diagram, the threats from prompt injection, excessive agency, data exfiltration, confused-deputy and memory poisoning, and the mitigations, mapped to the OWASP Top 10 for LLM Applications 2026 and the OWASP Top 10 for Agentic Applications 2026.