Frontend Handbook
Written

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.

01

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 a javascript: 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 unchecked href, or injecting into markup it doesn't own all reopen the hole. innerHTML won'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

Sink

The value becomes markup. Any tags and event handlers in it are live.

Runs only inside the sandbox below (its own opaque origin); it can't touch this page.

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)

02

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-src and the rest each say what that kind of resource may come from. Start from default-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-response nonce (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-dynamic lets 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-Only reports 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

script-src

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-src with strict-dynamic, roll out in report-only, and keep default-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)

03

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) and Strict keep 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. But Lax still 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-site fetch can only after the server opts in through CORS.
  • Why a bearer token sidesteps classic CSRF. A JWT in an Authorization header 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

Session cookie SameSite
CSRF defence
  • Do: Use SameSite=Lax (or Strict) cookies and a CSRF token for state-changing requests; require a custom header for JSON APIs.

    Avoid: Relying on SameSite alone, 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)