Overview
Cross-site scripting (XSS) happens when an attacker gets their JavaScript to run in other users' browsers on your site. Because the script runs with your site's origin, it can read page data, act as the user, steal tokens stored in JavaScript-accessible storage, or deface the page. The three types are stored XSS (malicious input saved and shown to others, like a comment), reflected XSS (input echoed from the URL into the response), and DOM-based XSS (client-side code writing untrusted data into the page).
The core defense is context-aware output encoding, meaning untrusted data is always rendered as text, never as HTML or script. Modern frameworks like React escape by default; risk returns with escape hatches (dangerouslySetInnerHTML, innerHTML, v-html), URLs like javascript:, and inline event handlers. Content Security Policy (CSP), HttpOnly cookies, and HTML sanitizers add defense in depth.
If the building lets anyone pin notes, a malicious note could say "security says: hand your keys to the person in the red jacket." Encoding is laminating every note so it is read as a note, not treated as an official instruction.
When to use it
- Any page that displays user-provided content.
- Rich text editors, comments, profiles, markdown rendering.
- Security reviews of front-end code.
Where it shows up in interviews
Recognize it when: user text shown to other users.
- Design a comments system
- Design a social profile page
- Design a markdown-based wiki
Where it is used in real software
A stored XSS payload added over a million friends in a day, showing how XSS can spread virally.
Escape interpolated values by default; security bugs usually come from bypassing that with raw HTML APIs.
Google, GitHub, and others deploy strict CSP to block inline and unauthorized scripts even if an injection slips through.
Key terms
- Stored XSS
- Payload persisted and served to other users.
- Reflected XSS
- Payload in a request echoed back in the response.
- DOM XSS
- Client-side code inserts untrusted data into the DOM unsafely.
- Output encoding
- Converting characters like < and " into safe entities for the context.
- CSP
- Header restricting which scripts and resources the page may load or run.
How it works, step by step
- 1Treat all user data as untrusted
Including data from your own database, since it may have been stored by an attacker.
- 2Render as text by default
Use framework interpolation or textContent, never innerHTML for untrusted data.
- 3Sanitize when HTML is required
Use a proven sanitizer (DOMPurify) with an allow-list of tags and attributes.
- 4Validate URLs
Allow only http(s) and relative links; block javascript: and data: URLs in href and src.
- 5Add defense in depth
Strict CSP with nonces, HttpOnly and SameSite cookies, and security headers.
STEP 1The attacker posts a comment containing a script tag.
Unsafe vs safe rendering
Displaying a user comment
| Approach | Payload shown as | Safe? |
|---|---|---|
| element.innerHTML = comment | Executing HTML and script | No |
| element.textContent = comment | Literal text | Yes |
| React: <p>{comment}</p> | Escaped text | Yes |
| React: dangerouslySetInnerHTML={{ __html: comment }} | HTML | Only after sanitizing |
| <a href={userUrl}> with javascript: URL | Script on click | No; validate the scheme |
NOWApproach: element.innerHTML = comment | Payload shown as: Executing HTML and script | Safe?: No
The default text rendering is safe; each escape hatch needs sanitization or validation.
Implementation
import DOMPurify from "dompurify"; // Safe by default: React escapes interpolated stringsfunction Comment({ author, body }: { author: string; body: string }) { return <p><strong>{author}</strong>: {body}</p>;} // Rich text: sanitize with an allow-list before rendering HTMLfunction RichComment({ html }: { html: string }) { const clean = DOMPurify.sanitize(html, { ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "ul", "li", "p"], ALLOWED_ATTR: ["href"] }); return <div dangerouslySetInnerHTML={{ __html: clean }} />;} // Links: allow only safe schemesfunction SafeLink({ href, children }: { href: string; children: React.ReactNode }) { const ok = /^(https?:\/\/|\/(?!\/))/i.test(href); return ok ? <a href={href} rel="noopener noreferrer">{children}</a> : <span>{children}</span>;}Complexity and performance
Negligible.
Script acts as the victim on your site.
Trade-offs
Allowing HTML enables formatting but requires careful sanitization; Markdown with a safe renderer is often a better compromise.
Strict CSP blocks most XSS but can break third-party scripts and inline code; roll out in report-only mode first.
Variants and related techniques
Browser API that forces DOM sinks like innerHTML to accept only vetted values.
Render untrusted HTML in an isolated origin.
Common mistakes
- Sanitizing on input only.
Fix: Encode on output for the specific context; data may be used in many contexts later.
- Storing auth tokens in localStorage.
Fix: Any XSS can read them; prefer HttpOnly cookies with SameSite and CSRF protection.
- Trusting framework escaping in attribute URLs.
Fix: Validate URL schemes; escaping does not stop javascript: links.
Interview questions
What are the types of XSS and how do you prevent them?
Stored, reflected, and DOM-based. Prevent them by rendering untrusted data as text with context-aware encoding (framework defaults), sanitizing any allowed HTML with an allow-list, validating URLs, avoiding dangerous DOM APIs, and adding CSP and HttpOnly cookies as defense in depth.
Does React prevent all XSS?
React escapes interpolated values, which prevents most XSS, but dangerouslySetInnerHTML, unsafe href values like javascript: URLs, and direct DOM manipulation can still introduce it.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Find and fix XSS in a comments component | Easy | Safe rendering. |
| Design safe rich-text comments with Markdown | Medium | Sanitization and CSP. |