XSS & CSRF Attacks

CS 360 — Cross-Site Scripting, Cross-Site Request Forgery, and how cookies protect you

Not logged in Log in via the Auth page first
1

Setup: Create Cookies with Different Protections

Set four cookies with different security attributes, then see which ones XSS and CSRF can exploit.

Cookie security attributes are your defense. We'll set four cookies on this origin, each with different protections:

regular_session — HttpOnly=false, SameSite=Lax (readable by JS, sent on navigations)
httponly_session — HttpOnly=true, SameSite=Lax (invisible to JS!)
strict_session — HttpOnly=false, SameSite=Strict (never sent cross-site)
none_session — HttpOnly=false, SameSite=None (sent everywhere, most vulnerable)

Console exercise: inspect your cookies

Open Chrome DevTools (F12) and try these in the Console tab:

document.cookie

Notice: httponly_session is missing from the output — HttpOnly cookies are invisible to JavaScript. But check DevTools → Application → Cookies — it's there, and the browser sends it on every request.

document.cookie.split(';').map(c => c.trim().split('=')[0])

Lists just the cookie names visible to JS.

2

XSS: Cross-Site Scripting

Inject JavaScript into a vulnerable page and steal cookies visible to JS.

XSS (Cross-Site Scripting): An attacker injects malicious JavaScript into a web page. The injected script runs in the victim's browser with full access to that page's DOM, including document.cookie. This lets the attacker steal session tokens, modify the page, or perform actions as the user.

Demo: Reflected XSS via vulnerable search

The server has a /api/xss/search?q=... endpoint that reflects user input without escaping. Type a normal search, then try injecting HTML/JS:

Server response rendered as HTML:


Try these XSS payloads

Paste each into the search box and click Search (Vulnerable). Watch the iframe and your console.

1. Basic injection — inject bold HTML:

<b style="color:red;font-size:24px">INJECTED!</b>

2. Script injection — pop an alert:

<img src=x onerror="alert('XSS!')">

3. Cookie theft — steal document.cookie:

<img src=x onerror="fetch('/api/xss/search?q='+encodeURIComponent(document.cookie))">

Check the Network tab — the stolen cookies are sent as a query parameter. In a real attack, this would go to the attacker's server.

4. DOM manipulation — deface the page:

<img src=x onerror="document.body.innerHTML='<h1 style=color:red>HACKED</h1>'">

Console exercise: see what XSS can and cannot steal

Open the Console (F12) and run these directly. Compare what's visible:

// What an XSS attacker can see: console.log("Cookies visible to JS:", document.cookie); console.log("An attacker would send these to their server!");
// Try to find the HttpOnly cookie: console.log("Can I see httponly_session?", document.cookie.includes("httponly_session")); // Always false! HttpOnly cookies are invisible to JavaScript.
// Check DevTools → Application → Cookies to see ALL cookies // including HttpOnly ones the browser sends but JS can't read
Defense: HttpOnly cookies. The httponly_session cookie is invisible to document.cookie and all JavaScript. Even if XSS executes, it cannot steal this cookie. That's why session tokens should always be HttpOnly.

Defense: Output escaping. Click "Search (Safe)" to see how proper HTML escaping prevents the script from executing at all — the payload is displayed as harmless text.
3

CSRF: Cross-Site Request Forgery

A malicious page on port 5051 performs actions on port 5050 using YOUR cookies.

CSRF (Cross-Site Request Forgery): An attacker tricks your browser into sending a request to a site where you're logged in. Because the browser automatically attaches cookies, the server thinks the request came from you. Unlike XSS, the attacker never sees your cookies — they just trick the browser into using them.
Prerequisites: You must be logged in on the Auth page first. The CSRF attack will use your session cookie to add notes to your account without your consent.

Your notes (legitimate feature)

Add a note normally, then see what happens when the attacker page adds one for you.


Launch CSRF Attack

Click the button below to open the evil attacker page on port 5051. That page will automatically submit a hidden form that POSTs to this server (port 5050). Since your browser has a session cookie for port 5050, it may be sent along automatically.

What just happened?
1. You visited the evil page on port 5051.
2. That page contained a hidden <form> that auto-submitted a POST to localhost:5050/api/csrf/add-note.
3. Your browser attached your session_token cookie (because the form targets port 5050).
4. The server saw a valid session cookie and added the note — thinking it was YOU.

Key insight: The attacker never saw your cookie. They just tricked the browser into using it. SOP blocks reading the response — but the POST already happened.

Console exercise: simulate CSRF yourself

Open console on the evil page (port 5051) and try:

// From the evil page, try to read the victim's cookies: document.cookie // Empty! The attacker can't see port 5050's cookies from port 5051.
// But a form submission DOES send them: let f = document.createElement('form'); f.method = 'POST'; f.action = 'http://localhost:5050/api/csrf/add-note'; let i = document.createElement('input'); i.name = 'note'; i.value = 'Console CSRF!'; f.appendChild(i); document.body.appendChild(f); f.submit();
4

Defense: SameSite Cookies

How the SameSite attribute controls whether cookies are sent on cross-site requests.

SameSite tells the browser when to include a cookie on cross-site requests:

StrictNever sent on cross-site requests. Not even on link clicks from another site. Maximum CSRF protection but can affect usability (e.g., clicking a link from email won't be logged in).

Lax (browser default) — Sent on top-level navigations (clicking links, typing URL) but NOT on cross-site form POSTs, iframes, or fetch. Good balance of security and usability.

None — Always sent, even on cross-site requests. Requires Secure flag. This is what the old pre-SameSite web looked like — fully vulnerable to CSRF.

Console exercise: see SameSite in action

Open DevTools → Application → Cookies → http://localhost:5050 and look at the SameSite column for each cookie.

// Check which cookies the browser sends to our own origin: fetch('/api/me', {credentials: 'include'}) .then(r => r.json()) .then(d => console.log('Same-site request cookies:', d.cookies_received));

All cookies are sent — because this is a same-site request.

// Now open the evil page's console (port 5051) and try: fetch('http://localhost:5050/api/me', {credentials: 'include'}) .then(r => r.json()) .then(d => console.log('Cross-site cookies:', d.cookies_received)) .catch(e => console.log('Blocked by CORS, but check Network tab for the request'));

On cross-site fetch: Strict cookies are NOT sent. Lax cookies are NOT sent (it's not a top-level navigation). Only None cookies would be sent.

CSRF protection matrix:

Attack vector SameSite=None SameSite=Lax SameSite=Strict
Cross-site form POST Cookie sent NOT sent NOT sent
Cross-site link (GET) Cookie sent Cookie sent NOT sent
Cross-site fetch/XHR Cookie sent NOT sent NOT sent
Iframe / embedded Cookie sent NOT sent NOT sent
Same-site (any) Cookie sent Cookie sent Cookie sent
5

Defense: CSRF Tokens

A server-generated secret that the attacker cannot guess or obtain.

CSRF tokens are random, per-session secrets that the server embeds in forms and checks on submission. The attacker's page cannot read this token (due to SOP), so their forged request will be missing the token and the server rejects it.

Test: Add note WITH CSRF token (protected)

This uses the /api/csrf/add-note-protected endpoint which requires a valid token.


Console exercise: try to forge a request

// From the evil page console (port 5051), try to use the protected endpoint: fetch('http://localhost:5050/api/csrf/add-note-protected', { method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'include', body: JSON.stringify({note: 'Forged!', csrf_token: 'i_guessed_this'}) }).then(r => r.json()).then(console.log); // Result: 403 Forbidden — "Invalid or missing CSRF token!"
// Can the attacker read the token from the victim's page? fetch('http://localhost:5050/api/csrf/token', {credentials: 'include'}) .then(r => r.json()).then(console.log) .catch(e => console.log('Blocked by SOP!', e)); // CORS blocks reading the response — attacker can't get the token.
Why CSRF tokens work:
1. The server generates a random token and gives it only to the legitimate page.
2. The legitimate page includes the token in every state-changing request.
3. The attacker's page cannot read the token (SOP blocks cross-origin reads).
4. Without the correct token, the server rejects the forged request.

Defense in depth: Use SameSite=Lax cookies (blocks most CSRF) AND CSRF tokens (catches edge cases like SameSite=None or older browsers).
6

Summary: XSS vs CSRF

Two different attacks, two different defense strategies.

XSS CSRF
What Inject malicious JS into the page Trick browser into sending a forged request
Attacker sees cookies? Yes (non-HttpOnly) No — just tricks browser into using them
Runs code on victim page? Yes No
Primary defense Output escaping + HttpOnly cookies + CSP SameSite cookies + CSRF tokens
Cookie attribute HttpOnly prevents JS from reading SameSite prevents cross-site sending