XSS, CSP and CSRF
How untrusted content becomes running script and how encoding stops it, what a Content-Security-Policy does as a second wall, and how another site makes a request as the logged-in user, each with a live demo.
Three attacks sit at the centre of frontend security. XSS runs the attacker's script in your page; CSP is the browser-enforced wall that limits the damage when it slips through; and CSRF makes the browser send a request as the logged-in user. They connect: XSS defeats almost every other defence, CSP is how you contain it, and CSRF is the cookie-session counterpart to where you keep a token. The attacks below really run, inside a sandbox that can't touch this page.
XSS: when content becomes script
Cross-site scripting is an output-encoding bug: untrusted data is written into a page in a place where the browser reads it as code instead of text. The fix is never "filter the input" — it is to encode every value for the exact context it lands in.
- Context decides danger. The same string is harmless as a text node, dangerous inside
innerHTML, dangerous again when it breaks out of an attribute, and dangerous as ajavascript:URL. Each context needs its own encoding. - Three shapes. Stored (the payload is saved and served to everyone), reflected (bounced back from a URL or form), and DOM-based (client code writes untrusted data into a sink). All three are the same root cause.
- Frameworks escape by default — until you opt out. React escapes text in JSX, but
dangerouslySetInnerHTML, an uncheckedhref, or injecting into markup it doesn't own all reopen the hole.innerHTMLwon't run a late<script>, but it will run an<img onerror>. - Then it's game over. A script in your page can read tokens, call your API as the user, and rewrite the page. That is why the token page kept saying to stop XSS first.
Try it: inject through different sinks, then encode
The attacker's input
How the app renders it
The value becomes markup. Any tags and event handlers in it are live.
The sandboxed app (opaque origin)
Render a payload to see whether it executed.
Do: Let the framework escape; encode per context for the rest; validate URL schemes; keep untrusted HTML out, or sanitize it with a vetted library.
Avoid: Building HTML by string concatenation, trusting a "sanitized" input once, or putting untrusted data in a
<script>, a style, or an event handler.
Read more:OWASP: XSS prevention(opens in a new tab)OWASP: DOM-based XSS(opens in a new tab)MDN: Trusted Types(opens in a new tab)
CSP: a second wall
A Content-Security-Policy is an HTTP header that tells the browser which sources of script, style, images and more it may load, and the browser enforces it. It's the main defence-in-depth for XSS: even if a payload lands, a good policy stops it executing.
- Allowlists by directive.
script-src,img-src,default-srcand the rest each say what that kind of resource may come from. Start fromdefault-src 'none'and open only what you need. - Nonces and hashes beat
unsafe-inline.'unsafe-inline'allows every inline script, which defeats the point. A per-responsenonce(or a hash of the script) allows only the scripts you marked. If a nonce or hash is present, the browser ignores'unsafe-inline'. strict-dynamiclets a trusted (nonced) script load more scripts without maintaining host allowlists — the modern recommended shape, with a nonce as the anchor.- Report-only first.
Content-Security-Policy-Report-Onlyreports violations without blocking, so you can tighten a policy on a live site safely. CSP is a second wall, not a substitute for encoding.
Try it: build a policy and watch it block
Build a policy
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; script-src 'unsafe-inline'
The sample page (served with the real header)
Serve the page to see which resources the policy allows.
Do: Serve a nonce-based
script-srcwithstrict-dynamic, roll out in report-only, and keepdefault-src 'none'.Avoid: Shipping
'unsafe-inline', treating CSP as your only XSS defence, or an allowlist so broad it permits an attacker-controlled host.
Read more:MDN: CSP(opens in a new tab)OWASP: CSP cheat sheet(opens in a new tab)
CSRF: a request made as you
Cross-site request forgery abuses the fact that the browser attaches your cookies to a request automatically, wherever it comes from. Another site can make your browser POST to an app you're logged into, and the server, seeing a valid session cookie, acts on it.
- It rides on ambient credentials. The attacker never sees your cookie; they just cause a request, and the browser adds the cookie. So CSRF targets cookie sessions, not tokens a script must add by hand.
SameSite, and now third-party-cookie blocking, are the first line.Lax(today's default) andStrictkeep the cookie off cross-site requests, and modern browsers increasingly block third-party cookies outright — so the classic cross-site attack often fails before it starts. ButLaxstill allows top-level GET navigations, a same-site subdomain can still send the cookie, and older clients don't enforce it — so this is not a complete defence.- Tokens the attacker can't guess or send. A synchronizer token in the form body, or a custom header the server requires — a cross-site
<form>can't set a custom header, and a cross-sitefetchcan only after the server opts in through CORS. - Why a bearer token sidesteps classic CSRF. A JWT in an
Authorizationheader is added by your code, not the browser, so a forged cross-site request simply arrives without it — the trade-off that sends you back to the XSS risk of storing it.
Try it: forge a request from another site, then defend
1. Sign in to the sample app
Do: Use
SameSite=Lax(orStrict) cookies and a CSRF token for state-changing requests; require a custom header for JSON APIs.Avoid: Relying on
SameSitealone, performing state changes on GET, or assuming an API is safe because it only accepts JSON.
Read more:OWASP: CSRF prevention(opens in a new tab)MDN: SameSite cookies(opens in a new tab)