{
  "version": 1,
  "key": "xss-output-encoding",
  "title": "Prevent XSS at the output boundary",
  "url": "https://softwaresecurity.com/domains/appsec/secure-coding/xss-output-encoding/",
  "markdown": "https://softwaresecurity.com/downloads/xss-output-encoding.md",
  "summary": "Choose text rendering, context-aware encoding, or deliberate HTML sanitization according to how the browser will interpret the value.",
  "answer": "Render untrusted data for the exact place it lands. Use text APIs such as textContent or escaped template interpolation for text; parse URLs and allowlist scheme and host before using them as links; keep untrusted strings out of inline scripts, event handlers and styles; and pass rich HTML through a maintained sanitizer. Review every framework escape hatch, such as dangerouslySetInnerHTML, v-html, |safe or Html.Raw. CSP and Trusted Types enforce these rules; they do not replace them.",
  "keyPoints": [
    "Text is the default: textContent or framework interpolation shows markup-shaped input as characters.",
    "Each context has its own rule: HTML text, attributes, URLs, JavaScript and CSS are different parsers.",
    "HTML escaping cannot make a javascript: URL safe; parse the URL and allowlist scheme and host.",
    "Every escape hatch that inserts raw HTML needs a sanitizer and a reviewer.",
    "Trusted Types, Baseline in current browsers since February 2026, make string assignments to HTML sinks fail; roll them out in report-only mode first."
  ],
  "verify": [
    "Render a name containing an image tag with an onerror attribute; assert the exact string is visible and no image element exists.",
    "Feed javascript:, a lookalike host, credentials and a non-default port to every link field; only the allowed URL may become an href.",
    "Search templates and components for raw-HTML escape hatches and confirm each input is sanitized or a constant.",
    "Repeat the checks on validation errors, administrator views and email previews, which often use different templates.",
    "Send Content-Security-Policy-Report-Only: require-trusted-types-for 'script' and fix every reported sink before enforcing it."
  ],
  "identifiers": {
    "cwe": [
      "CWE-79",
      "CWE-116"
    ],
    "owaspTop10_2025": [
      "A05:2025"
    ],
    "asvs5": [
      "V1.2.1",
      "V1.2.2",
      "V1.3.1",
      "V3.2.2"
    ],
    "capec": [
      "CAPEC-63",
      "CAPEC-588",
      "CAPEC-591",
      "CAPEC-592"
    ],
    "wstg": [
      "WSTG-INPV-01",
      "WSTG-INPV-02"
    ]
  },
  "published": "2026-10-07",
  "updated": "2026-10-10",
  "domain": {
    "key": "appsec",
    "name": "Application security"
  },
  "type": "Guide",
  "appliesTo": "HTML, attribute, URL and script output boundaries. An HTML encoder is not a universal sanitizer.",
  "tags": [
    {
      "key": "xss",
      "label": "Cross-site scripting (XSS)"
    },
    {
      "key": "browser-security",
      "label": "Browser security"
    }
  ],
  "stages": [
    "build"
  ],
  "roles": [
    "developer"
  ],
  "sections": [
    {
      "id": "context",
      "title": "Follow the string into the parser that receives it",
      "url": "https://softwaresecurity.com/domains/appsec/secure-coding/xss-output-encoding/#context",
      "text": "Cross-site scripting occurs when an application turns untrusted data into browser-executable content in its own origin. The same characters have different meanings in text, HTML attributes, URLs, JavaScript and CSS. A universal replace-angle-brackets function cannot secure all those contexts. First identify the final sink: is a string assigned as text, parsed as HTML, used as a link destination or inserted into executable source?\n\nFor a plain customer name, the requirement is to show characters. Assigning name through textContent creates a text node. Assigning the same name through innerHTML invokes the HTML parser. The supplied string <img src=x onerror=\"window.demo=1\"> therefore becomes visible literal text in the first case and an element with an event handler in the second. Do not judge the vulnerable case only by script tags: event handlers and other active contexts can execute without a script element. Stored input is still untrusted when an administrator later views it.\n\nIn the browser, the same reasoning applies to DOM-based XSS, where no server template is involved. Sources such as location.hash, location.search, document.referrer, window.name and postMessage data are attacker-influenced. Sinks such as innerHTML, outerHTML, insertAdjacentHTML, document.write, srcdoc, eval, setTimeout with a string and new Function interpret what they receive. Trace each source to its sink exactly as you would a server value."
    },
    {
      "id": "controls",
      "title": "Choose an interface that preserves the intended type",
      "url": "https://softwaresecurity.com/domains/appsec/secure-coding/xss-output-encoding/#controls",
      "text": "For text, use a text node or the framework's ordinary escaped interpolation. For an attribute, use a fixed safe attribute name and the framework's attribute API; validation still matters for attributes that carry URLs or other interpretable values. For a profile link, parse the URL, require an allowed scheme and any required host policy, then assign the resulting URL. HTML escaping does not make javascript: an acceptable link. Avoid placing untrusted data inside inline JavaScript, style source or event-handler attributes.\n\nWhen a feature genuinely requires rich text, use a maintained sanitizer, such as DOMPurify or the browser's Element.setHTML(), configured for that feature's allowed elements and attributes (the lab at /labs/html-sanitizer/ has you configure one), and sanitize the final representation before the HTML sink. A sanitizer is a parser-aware policy mechanism, not a regular expression deleting a few tag names. Keep the library updated and do not append new untrusted markup after sanitization. CSP can reduce some exploitation paths and reveal violations, but it does not make an unsafe output boundary correct; a test that passes only because one browser blocked execution has not shown correct output handling.\n\nTrusted Types turn the sink rule into an enforced one. With require-trusted-types-for 'script' in the Content Security Policy, assigning a plain string to innerHTML and the other script-capable sinks throws a TypeError, so each remaining sink must go through a named, reviewed policy. The feature has been Baseline across current browsers since February 2026. A policy must still sanitize or encode: one that returns its input unchanged only moves the problem.",
      "code": {
        "language": "javascript",
        "text": "// Browser JavaScript: ordinary text and a constrained absolute link.\nconst label = document.createElement('span');\nlabel.textContent = suppliedName;\n\nconst link = document.createElement('a');\nconst parsed = new URL(suppliedUrl);\nif (parsed.protocol !== 'https:' || parsed.hostname !== 'docs.example.test'\n    || parsed.username || parsed.password || parsed.port) {\n  throw new Error('Unsupported destination');\n}\nlink.href = parsed.href;\nlink.textContent = 'Read documentation';"
      }
    },
    {
      "id": "frameworks",
      "title": "Know the escape hatch in your framework",
      "url": "https://softwaresecurity.com/domains/appsec/secure-coding/xss-output-encoding/#frameworks",
      "text": "Modern frameworks escape interpolated values by default, so most XSS findings in framework code sit at a deliberate escape hatch. Treat each one as a raw HTML sink: the value must be a constant, or the output of a maintained sanitizer configured for that feature. URL attributes such as href and src need the separate scheme check shown above, whatever the framework; /fix/xss/ has the repair for each common stack.\n\nFramework: React; Escaped by default: {value} in JSX; Escape hatch to review: dangerouslySetInnerHTML\nFramework: Vue; Escaped by default: {{ value }}; Escape hatch to review: v-html\nFramework: Angular; Escaped by default: Interpolation; [innerHTML] is sanitized; Escape hatch to review: DomSanitizer.bypassSecurityTrustHtml and the other bypassSecurityTrust methods\nFramework: Svelte; Escaped by default: {value}; Escape hatch to review: {@html value}\nFramework: Jinja2 and Flask; Escaped by default: {{ value }} with autoescaping; Escape hatch to review: |safe, Markup(), {% autoescape false %}\nFramework: Django templates; Escaped by default: {{ value }}; Escape hatch to review: |safe, mark_safe(), {% autoescape off %}\nFramework: Ruby on Rails ERB; Escaped by default: <%= value %>; Escape hatch to review: raw, .html_safe, <%== value %>\nFramework: ASP.NET Core Razor; Escaped by default: @value; Escape hatch to review: Html.Raw()\nFramework: Thymeleaf; Escaped by default: th:text; Escape hatch to review: th:utext\nFramework: Go html/template; Escaped by default: Contextual escaping; Escape hatch to review: template.HTML, template.JS and template.URL conversions\nFramework: Laravel Blade; Escaped by default: {{ $value }}; Escape hatch to review: {!! $value !!}\nFramework: Handlebars; Escaped by default: {{value}}; Escape hatch to review: {{{value}}}"
    },
    {
      "id": "verify",
      "title": "Verify structure and behavior rather than one alert box",
      "url": "https://softwaresecurity.com/domains/appsec/secure-coding/xss-output-encoding/#verify",
      "text": "Use a test record containing ordinary punctuation, markup-shaped text and a disallowed URL. After rendering, inspect textContent and the DOM tree. For the name field, the exact submitted characters must be visible and no img or script node may be created from them. For the link field, an unsupported scheme must not create a clickable destination. The normal HTTPS documentation link must continue to work. These assertions distinguish a repaired renderer from a feature that simply stopped rendering. To practice the repair in a real DOM, the browser lab at /labs/output-rendering/ has you render a model’s answer as text and add a source link only for an allowed URL; it replays your DOM writes in a real document and checks the text, the elements and the link, including javascript:, lookalike-host, userinfo and port URLs.\n\nCheck every presentation of the record: search results, notifications, export previews, error messages and staff dashboards often use different templates. A safe React or template interpolation can be undone by a raw-HTML escape hatch or a third-party component that interprets the string again. Double encoding is a common symptom of storing already-escaped text and escaping it again at output: users see &amp;lt; rather than their original characters. Store the intended data representation and encode for its destination when rendering. Logs and analytics are also output systems and need their own safe viewers."
    },
    {
      "id": "real-world-failures",
      "title": "A real-world failure",
      "url": "https://softwaresecurity.com/domains/appsec/secure-coding/xss-output-encoding/#real-world-failures",
      "text": "Stored cross-site scripting still ships in mature software. CVE-2024-4439 was a stored XSS in WordPress core (up to 6.5.1): a user display name was rendered in the Avatar block without output escaping, and comment avatars made it reachable by unauthenticated visitors. The missing control was contextual output encoding of the display name."
    },
    {
      "id": "failure-analysis",
      "title": "Check transformations and exceptional rendering paths",
      "url": "https://softwaresecurity.com/domains/appsec/secure-coding/xss-output-encoding/#failure-analysis",
      "text": "A value can cross more than one parser. Suppose a JSON response contains a safe string, the page concatenates that string into an HTML fragment and then assigns the fragment through innerHTML. JSON serialization protected the JSON grammar, but the subsequent HTML sink grants a new interpretation. Likewise, a URL that was valid when stored can become unsafe if code later decodes or rewrites it before navigation. Place the control at the final relevant boundary and record which representation it protects.\n\nError messages are a useful review target because they often echo invalid input outside the normal component system. If the main profile page uses textContent but a validation banner builds 'Invalid name: ' + suppliedName as raw HTML, the same input still executes through the rejected-input path. Test both acceptance and rejection rendering, and repeat the check in administrator views and email previews where template choices may differ.\n\nAvoid treating character removal as a security contract. Removing < and > from every name would change valid text and would not repair a javascript: link. Encoding an already-encoded string twice can also break useful content without improving the intended boundary. Compare exact text nodes and attributes after the browser parses the response. For the exercise's N2 string, the robust result is a single text representation containing the full supplied characters and zero generated img nodes, regardless of whether an image request or event happened to be observable in a particular run.\n\nIf the product later chooses to allow a markup subset, that is a new rendering capability. Add a dedicated sanitizer policy and tests; do not mark all previously stored names safe because their source was an authenticated account."
    }
  ],
  "sources": [
    {
      "title": "OWASP Cross Site Scripting Prevention",
      "url": "https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html"
    },
    {
      "title": "MDN: innerHTML security considerations",
      "url": "https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML"
    },
    {
      "title": "MDN: Trusted Types API",
      "url": "https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API"
    },
    {
      "title": "OWASP DOM based XSS Prevention Cheat Sheet",
      "url": "https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html"
    },
    {
      "title": "React: dangerouslySetInnerHTML",
      "url": "https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html"
    },
    {
      "title": "Angular: security and DomSanitizer",
      "url": "https://angular.dev/best-practices/security"
    },
    {
      "title": "NVD: CVE-2024-4439, stored XSS in WordPress core",
      "url": "https://nvd.nist.gov/vuln/detail/cve-2024-4439"
    }
  ],
  "license": "https://creativecommons.org/licenses/by/4.0/",
  "attribution": "Prevent XSS at the output boundary, Software Security, https://softwaresecurity.com/domains/appsec/secure-coding/xss-output-encoding/, CC BY 4.0"
}