Secure Development · 2026
XSS Prevention Guide: Stop Cross-Site Scripting
Cross-site scripting lets an attacker run their JavaScript in your users' browsers — hijacking sessions, stealing data, and rewriting the page. This is the practitioner's guide to preventing it: contextual output encoding, framework auto-escaping, Content Security Policy, and Trusted Types — with code and MITRE ATT&CK mapping.

Quick answer
Prevent cross-site scripting by encoding untrusted data for the exact context it lands in — HTML, attribute, JavaScript, URL, or CSS — and by relying on a modern framework's automatic escaping. Sanitize any HTML you must render with a vetted library, validate URL schemes, and avoid dangerous DOM sinks. Add a strict, nonce-based Content Security Policy as defense in depth. Then verify with authenticated penetration testing.
Cross-site scripting is an injection flaw where untrusted input is rendered into a page in a way that lets it execute as script in the victim's browser. It is one of the longest-standing entries on the OWASP Top 10 and shares its root cause with SQL injection: mixing data and code. We build front ends for a living — our web application practice bakes these defenses in, and our web app pentest verifies them. The sections below follow the order that matters in practice.
1. Contextual output encoding: the core defense
The defining rule of XSS prevention is to encode untrusted data for the context where it is inserted. The same value needs different treatment in an HTML body, an HTML attribute, a JavaScript string, a URL, or a CSS value. Encoding for the wrong context — or not at all — is the bug.
// VULNERABLE — raw user input written into the DOM as HTML
element.innerHTML = "Welcome, " + userName;
// userName = <img src=x onerror=alert(document.cookie)> executes
// FIXED — assign as text; the browser never parses it as markup
element.textContent = "Welcome, " + userName;- Prefer safe sinks:
textContentoverinnerHTML, and let the framework interpolate rather than building HTML strings. - Encode for the specific context; HTML-encoding a value placed inside a
<script>block does not make it safe. - Validate URL schemes — reject
javascript:anddata:inhrefandsrcattributes.
ATT&CK link: XSS supports Drive-by Compromise (T1189) and theft of Web Session Cookies (T1539).
2. Framework auto-escaping, and where it leaks
Modern frameworks escape interpolated values by default, which is why framework-based UIs are generally safer. The leaks are the explicit escape hatches: raw-HTML insertion, attribute injection, and handing untrusted data to third-party DOM code.
// VULNERABLE — raw HTML from an untrusted source
<div dangerouslySetInnerHTML={{ __html: comment.body }} />
// FIXED — sanitize first with a vetted library
import DOMPurify from "dompurify";
<div
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comment.body) }}
/>- Avoid
dangerouslySetInnerHTML(React) andv-html(Vue) on untrusted data. When unavoidable, sanitize with DOMPurify. - Never interpolate user data into a
<script>tag or an inline event handler. - Keep the framework patched — escaping behavior and known bypasses do change between versions.
3. DOM-based XSS: the bug the server never sees
DOM-based XSS happens entirely in the browser: client-side code reads from an attacker-controlled source — the URL fragment, query string, or postMessage data — and writes it into a dangerous sink. Because the payload may never hit the server, server-side filtering and many scanners miss it entirely.
- Avoid dangerous sinks:
innerHTML,document.write,eval, andsetTimeoutwith a string argument. - Treat
location,document.referrer, and message events as untrusted sources. - Adopt Trusted Types where supported — it enforces that only sanitized values can reach dangerous sinks, turning a silent bug into a build-time error.
ATT&CK link: client-side execution maps to Drive-by Compromise (T1189).
4. Content Security Policy and cookie hardening
A strict Content Security Policy is your safety net: even if an injection slips through, a good CSP can stop the malicious script from running. Prefer a nonce- or hash-based policy over domain allow-lists, which are easy to misconfigure into uselessness.
// Nonce-based CSP: only scripts with this request's nonce execute
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m';
object-src 'none';
base-uri 'self'- Set session cookies
HttpOnlyso a script cannot read them, plusSecureandSameSite. - Roll CSP out in
Report-Onlymode first to catch violations without breaking the site. - Pair CSP with a sound API security posture — the front end and the API defend different layers of the same app.
Mid-post: prove the encoding holds
Encoding and CSP are the fix. An authenticated pentest that injects context-aware payloads proves none of your sinks execute. Book a free scoping call.
The three types of XSS at a glance
| Type | What it means |
|---|---|
| Stored | Payload saved server-side and served to every user who views it |
| Reflected | Payload in the request is echoed straight back in the response |
| DOM-based | Client-side script writes untrusted data into a dangerous sink |
| Mutation (mXSS) | Browser re-parsing mutates "safe" HTML back into an executing payload |
For where XSS fits the wider risk list, see the OWASP Top 10 explained.
Operational practices that hold over time
XSS creeps back in as a UI grows. Three habits keep it out:
- Lint the sinks. Use ESLint rules that flag
innerHTMLanddangerouslySetInnerHTMLso every use is a deliberate, reviewed decision. - Centralize sanitization. Wrap DOMPurify in one helper so the whole team uses the same vetted configuration.
- Regular testing. Re-test after UI changes. The difference between a scan and a real test is in pen test vs vulnerability scan.
XSS rarely travels alone. The server-side companion is preventing SQL injection, and for SaaS teams the broader program is in cybersecurity services for SaaS startups.
Frequently asked questions
What is the best way to prevent cross-site scripting?
Contextual output encoding combined with a modern framework's automatic escaping. Encode untrusted data at the point it is inserted into a page, using the encoding appropriate for that context — HTML body, attribute, JavaScript, URL, or CSS. Frameworks like React, Angular, and Vue auto-escape interpolated values by default, which closes the common case. Layer a strict Content Security Policy on top as defense in depth, and never disable a framework's escaping without sanitizing first.
What are the three types of XSS?
Stored XSS, where a payload is saved on the server and served to other users; reflected XSS, where a payload in the request is immediately echoed back in the response; and DOM-based XSS, where client-side JavaScript writes untrusted data into the page without it ever reaching the server. Stored is usually the most damaging because it hits every visitor; DOM-based is the easiest to miss because server-side filtering never sees the payload.
Does React prevent XSS automatically?
React escapes values you interpolate into JSX by default, which prevents the most common injection. It does not protect you everywhere: dangerouslySetInnerHTML inserts raw HTML, untrusted values in href or src attributes can carry javascript: URLs, and passing user data into refs or third-party DOM libraries bypasses React entirely. Treat auto-escaping as a strong baseline, sanitize any HTML you must render with a library like DOMPurify, and validate URL schemes.
What is a Content Security Policy and does it stop XSS?
A Content Security Policy is an HTTP response header that tells the browser which sources of script, style, and other resources are allowed to load and execute. A strict CSP — ideally nonce- or hash-based rather than allow-listed domains — blocks inline script and unauthorized sources, so even if an injection slips through, the malicious script often will not run. CSP is a powerful mitigation, but it is a second line of defense, not a substitute for output encoding.
What is DOM-based XSS and why is it harder to catch?
DOM-based XSS happens entirely in the browser: client-side JavaScript reads from a source the attacker controls — such as the URL fragment or query string — and writes it into a dangerous sink like innerHTML, document.write, or eval, without sanitization. Because the payload may never be sent to the server, server-side filtering and many scanners miss it. The defense is to avoid dangerous sinks, use safe DOM APIs like textContent, and adopt Trusted Types where supported.
How do you test an application for XSS?
With authenticated penetration testing that injects context-aware payloads into every input — form fields, URL parameters, headers, and stored values — and observes where they execute, including in the DOM. A tester confirms the payload actually runs rather than just appearing in the response, distinguishing exploitable XSS from harmless reflection. Findings are mapped to OWASP and the relevant MITRE ATT&CK technique so engineers can prioritize and verify the fix.
Sources & references
- [1]OWASP Cross Site Scripting Prevention Cheat Sheet · OWASP
- [2]OWASP DOM Based XSS Prevention Cheat Sheet · OWASP
- [3]Content Security Policy (CSP) · MDN Web Docs
- [4]MITRE ATT&CK Enterprise Matrix · MITRE
Related reading and next steps
- Web Application Pentest service
- Web Application Development service
- Penetration Testing service overview
- The OWASP Top 10 explained (2026)
- Preventing SQL injection (2026)
- Securing REST APIs (2026)
- API security best practices (2026)
- Cybersecurity services for SaaS startups
- Pen test vs vulnerability scan
- Talk to Bill about your application security
Encode it, then prove it holds.
An authenticated web app pentest injects context-aware payloads and maps every finding to the ATT&CK technique it enables. Book a free scoping call and we'll cover the right depth for your app.
More engineering security reading
All postsPreventing SQL Injection in Modern Web Apps (2026)
Parameterized queries, ORMs, least-privilege DB roles, and why concatenation still breaches apps.
Read postThe OWASP Top 10 Explained (2026)
Every category in plain English, with a real example and the concrete defense.
Read postAPI Security Best Practices (2026)
Auth, rate limiting, input validation, secrets, and the OWASP API Top 10.
Read post