Intermediate · Backend security · topic 38 of 54 · 30 min
XSS and CSRF: attacks through the browser
Explanation
XSS: someone else's script on your page
A script is a piece of JavaScript code. XSS happens when your page shows user text as HTML, and that text contains a <script> tag or similar. The browser cannot tell your code from the attacker's, so it runs it. The script can read the page, steal data, or send requests as the user.
- Stored XSS: the attacker saves the script in your database, for example in a product review. Every visitor who opens that page runs it.
- Reflected XSS: the script is in a link, like /search?q=<script>...</script>. The page repeats the search word back, so whoever clicks the link runs it.
Stop XSS: escape output
Escaping means turning special characters into safe codes so the browser shows them as text. For example < becomes < . The browser then displays a < sign instead of starting a tag.
| Character | Escaped as | Why |
|---|---|---|
| & | & | Starts an escape code itself, so it must be done first |
| < | < | Starts a tag |
| > | > | Ends a tag |
| " | " | Ends an attribute value |
| ' | ' | Ends an attribute value |
// UNSAFE: user text is read as HTML
results.innerHTML = '<p>You searched for ' + query + '</p>';
// SAFE: user text is set as plain text
const p = document.createElement('p');
p.textContent = 'You searched for ' + query;
results.append(p);Template engines and frameworks like React escape text by default, which removes most XSS. The danger comes back when you switch it off, for example with dangerouslySetInnerHTML in React or a "raw" filter in a template. If you must show user HTML (like a rich-text blog post), clean it with a sanitiser library such as DOMPurify.
Two more layers help. A Content-Security-Policy (CSP) header tells the browser which places scripts may load from, so injected inline scripts are blocked. An HttpOnly cookie cannot be read by JavaScript at all, so even if XSS happens, the session cookie cannot be stolen.
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
Set-Cookie: sid=8f2c...; HttpOnly; Secure; SameSite=Lax; Path=/CSRF: a forged request with your cookie
Browsers send your cookies with every request to a site, even if the request starts on another site. CSRF uses this. You are logged in to your bank. You open a bad page in another tab. That page has a hidden form that posts "transfer 50,000 to the attacker" to the bank. Your browser adds your bank cookie, and the bank thinks you sent it.
<!-- on the attacker's site -->
<form action="https://bank.example.in/transfer" method="POST">
<input type="hidden" name="to" value="attacker-account">
<input type="hidden" name="amount" value="50000">
</form>
<script>document.forms[0].submit();</script>Stop CSRF: three layers
- SameSite cookies: set SameSite=Lax (or Strict) on the session cookie. The browser then does not send it on cross-site POST requests. Modern browsers treat cookies as Lax by default, but set it yourself.
- CSRF tokens: the server puts a random secret token in the session and in each form. On every POST, PUT, PATCH or DELETE it checks that the token sent matches the one in the session. The attacker's page cannot know it.
- Check Origin: browsers send an Origin header saying which site started the request. Reject state-changing requests whose Origin is not your own site.
Watch the lesson
XSS and CSRF: attacks through the browser · 1/7
Two attacks use the browser against your users: XSS and CSRF.
Prefer a video by a teacher? Find free videos on “xss and csrf explained for beginners” on YouTube ↗
In real life: A review that stole sessions
A food delivery app let users write restaurant reviews and showed them with innerHTML. One user wrote a review containing a script that sent document.cookie to their own server. Everyone who opened that restaurant page leaked their session (stored XSS). The team fixed it in three ways: they showed reviews with textContent, made the session cookie HttpOnly so scripts cannot read it, and added a Content-Security-Policy that blocks inline scripts. They also added SameSite=Lax and CSRF tokens to the "change address" form.
Practice (0/2 done)
Write escapeHtml(text) that replaces & with &, < with <, > with >, " with " and ' with '. Replace & first, so you do not escape your own escape codes twice.
Write checkCsrf(request, session). Return true for GET, HEAD and OPTIONS. For any other method return true only if request.headers.origin equals ALLOWED_ORIGIN AND request.headers["x-csrf-token"] equals session.csrfToken (and that token is not empty). Otherwise return false.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.