CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Overview
Cross-Site Scripting (XSS) occurs when an application writes untrusted data into a web page without encoding it for the context it lands in, so the browser parses attacker-supplied text as markup or script and runs it. The sink can be HTML content, an attribute, a JavaScript block, CSS, or a URL.
Relationship to Other CWEs
An XSS finding belongs on this page rather than on its parent CWE-74 (Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')), which MITRE discourages mapping to because it is too abstract to act on. It does not automatically belong one level down either: MITRE asks you not to "force a mapping to a lower-level Base/Variant simply to comply with" its stated preference for narrow entries. Use one of the children below when the finding genuinely is that case; use this page otherwise.
The pages around it differ by which step of getting untrusted data onto the page went wrong:
- CWE-79 (this page) - untrusted data reaches a browser-parsed context without being encoded for that context, whether the sink is the HTML body, an attribute, a script block, CSS, or a URL
- CWE-80 (Improper Neutralization of Script-Related HTML Tags in a Web Page (Basic XSS)) - the tag-injection case only:
<,>and&reaching element content unescaped. Same fix, narrower scope, and its page covers the blocklist attempts characteristic of it; anything landing in an attribute, script, CSS or URL context is this page - CWE-83 (Improper Neutralization of Script in Attributes in a Web Page) - the sink is an attribute value rather than element content, which changes the fix: attribute encoding, plus a scheme check on
hrefandsrcthat entity encoding alone will not give you - CWE-113 (HTTP Request/Response Splitting) - untrusted data reaches the response headers rather than the body, and the injected second response it can produce is one route to XSS. Output encoding does not close that; rejecting CR and LF in header values does
- CWE-352 (Cross-Site Request Forgery (CSRF)) - the neighbouring browser-trust attack, and the reason an XSS finding outranks a CSRF one: script running with the page's origin can read the anti-CSRF token and issue the request itself, so CSRF defenses are only as good as the absence of this weakness
MITRE gives CWE-79 five further children with no page here - CWE-81, CWE-84, CWE-85, CWE-86 and CWE-87. They name specific filter-evasion shapes: script in an error page, encoded URI schemes, doubled characters, invalid characters in identifiers, and alternate syntax. All of them are reasons blocklist filtering fails rather than separate fixes, so the context-correct encoding below is the answer for each.
OWASP Classification
A05:2025 - Injection
Risk
High to Critical: Injected script runs in the victim's browser with the page's own origin, so it can read the session cookies and tokens the page's JavaScript can reach, issue authenticated requests as the victim, and rewrite what the page shows - a login form that posts credentials elsewhere, for example.
Remediation Steps
Core Principle: Never render untrusted input directly into executable browser contexts; output-encode it for the specific context it lands in so it stays data, not script.
Trace the Data Path
Follow the untrusted data from where it enters to where the page renders it:
- Source: where untrusted data enters - user input, external files, databases, network requests, cookies, headers
- Data Flow: the transformations between source and output
- Sink: where the data is rendered - response writing, template rendering, DOM manipulation
- Output Context: where in the document it lands - HTML body, attribute, JavaScript, CSS, URL
- Missing Encoding: which encoding or escaping step, if any, sits between the two
Apply Context-Aware Output Encoding (Primary Defense)
Encoding by context:
- HTML Body: HTML entity encoding (e.g.,
<→<) - HTML Attributes: Attribute encoding (quote and escape special chars)
- JavaScript: JavaScript encoding (escape quotes, backslashes, etc.)
- URLs: URL encoding (percent-encode special characters)
- CSS: CSS encoding (escape CSS special characters)
Rules that hold in every context:
- Use framework-provided encoding functions, never write your own
- Encode at output time, not at input time
Use Safe APIs and Avoid Dangerous Functions
Prefer the framework's built-in protections over APIs that write raw markup:
Use safe defaults:
- Template engines with auto-escaping (Thymeleaf, Razor, Jinja2, etc.)
- Safe DOM manipulation (
.textContent, not.innerHTML) - Framework data binding that auto-escapes
Avoid dangerous functions:
- Never use:
eval(),.innerHTML,document.write()
Never Use Framework Security Bypasses with Untrusted Data
Frameworks provide "escape hatches" that bypass their XSS protection. Never use these with untrusted data:
- React:
dangerouslySetInnerHTML - Angular:
bypassSecurityTrustHtml(),bypassSecurityTrustScript(),bypassSecurityTrustUrl() - Vue.js:
v-htmldirective - Lit/Polymer:
unsafeHTML(),htmlLiteral() - Jinja2:
{{ data | safe }} - Thymeleaf:
th:utext - Razor:
@Html.Raw()
Any API with "unsafe", "raw", "bypass", "dangerously", or "trust" in its name is a warning sign. Use it only with:
- Trusted, server-generated content (never untrusted data)
- User-authored HTML that has been sanitized with DOMPurify (3.4.13 or later for JavaScript) or a similar allowlist-based sanitizer for the exact sink
Encoding covers the text, attribute, URL, JavaScript, and CSS contexts on its own; needing one of those is not a reason to reach for a raw HTML rendering API.
Add Input Validation and Content Security Policy (Defense in Depth)
Input Validation (supplementary control):
- Validate expected data format (email, phone, numeric, etc.)
- Use allowlists for enumerated values
- Constrain input to the expected shape; if rich HTML is allowed, sanitize it with an allowlist-based HTML sanitizer
- Never rely solely on input validation - encoding is still required
Content Security Policy (CSP):
- Set a strict CSP header that blocks inline scripts
- Disallow
unsafe-inlineandunsafe-eval - Use nonces or hashes for legitimate inline scripts
- Restrict script sources to trusted domains only
- CSP is defense-in-depth, not a replacement for encoding
- Do not set the legacy
X-XSS-Protectionheader: it is deprecated and ignored by current browsers, and CSP is its replacement
Test with XSS Payloads
Verify your encoding with attack vectors:
Basic XSS:
<script>alert(1)</script><img src=x onerror='alert(1)'><svg onload=alert(1)>
Context-specific payloads:
- Attribute injection:
" onclick="alert(1)" - JavaScript injection:
'; alert(1); // - URL injection:
javascript:alert(1)
Verification:
- Load the page with the malicious inputs
- Confirm each payload is displayed as text and not executed
- Check the browser console for JavaScript errors
- Inspect the encoded output in browser DevTools
- Confirm legitimate functionality still works
- Run automated scanners (OWASP ZAP, Burp Suite)
Common Vulnerable Patterns
Untrusted data is written directly into a response or the DOM without encoding:
// VULNERABLE - pseudo-code
output("<div>Welcome, " + username + "</div>")
// Attack: username = "<script>alert(document.cookie)</script>"
// Result: Script executes, stealing session cookies
Why this is vulnerable: the browser is not handed data and markup as two separate things. It receives one stream of bytes and decides what is a tag by parsing it, so a value placed into that stream is markup unless something has made it not be. username here is not "text that happens to contain angle brackets" - by the time the parser reaches it, it is a <script> element, indistinguishable from one the application wrote itself.
That is also why "encode the output" is not a single instruction. Which escaping makes a value safe depends on where in the document it lands: HTML-escaping is correct in element content, insufficient inside an unquoted attribute, wrong inside a <script> block where the parser is reading JavaScript, and beside the point in an href, where a javascript: scheme needs no special characters at all. An encoder is safe where it was written and unsafe where it was reused, which is the most common way a closed XSS finding comes back.
Secure Patterns
Encode the data for its output context before writing it, or use a template/DOM API that does this automatically:
// SECURE - pseudo-code
output("<div>Welcome, " + htmlEncode(username) + "</div>")
// Result: "<script>..." becomes "<script>..." (displayed as text)
Why this works: Output encoding turns <, >, &, ", and ' into their HTML entities (<, >, &, ", '), so the browser reads user input as text rather than as markup or script. Template systems with auto-escaping apply this at render time, so no single variable depends on someone remembering to encode it. Context-aware encoding does the same for HTML attributes, JavaScript, and URLs, each with the encoding that context needs. The language-specific guidance below names the APIs and framework features that provide this in each stack.
Common Pitfalls
- Blocklist filtering instead of encoding: Rejecting or stripping input that contains
<script>oron\w+=patterns looks like it blocks XSS, but attackers bypass denylists with case variation (<ScRiPt>), alternate vectors (<svg onload=...>,<img onerror=...>), or encoded payloads the filter never anticipated. Output encoding closes the whole class of attack; a blocklist only closes the examples the author thought of. - Encoding once at input and reusing the value in multiple output contexts: Sanitizing or HTML-encoding a value when it is received, then storing and later rendering that same value into HTML, JavaScript, an attribute, and a URL, protects only the context the original encoding targeted. A value safe in HTML body text can still break out of a
<script>block or anhrefattribute, so encoding has to happen at each output sink, not once at the source. - Using HTML-entity encoding for non-HTML contexts: Applying an HTML encoder to a value inserted into a JavaScript string, an inline event handler, a URL, or a CSS value leaves the attacker-controlled quotes, backslashes, or scheme intact for that context, because HTML entity encoding only neutralizes
<,>,&, and quotes for markup parsing, not JavaScript or URL syntax. - Treating Content Security Policy as the fix instead of a backstop: Deploying a CSP header and considering the finding resolved leaves the underlying unencoded output in place. A permissive or misconfigured policy (broad
script-srcallowances, missing coverage for an existing inline handler) still lets injected script run, and CSP does nothing for injection into attributes or URLs that do not require script execution to cause harm.
Language-Specific Guidance
Concrete APIs and framework examples for each stack:
- C# - ASP.NET Core, Razor with automatic encoding
- Go - html/template with auto-escaping, Gin, Echo
- Java - Spring Boot, JSP, Thymeleaf with context-aware escaping
- JavaScript/Node.js - Express, React, Vue, Angular with XSS prevention
- Perl - CGI, Catalyst with HTML escaping
- PHP - Laravel, Symfony with htmlspecialchars
- Python - Flask, Django, Jinja2 with autoescaping