XSS and CSRF
Two of the oldest web vulnerabilities. Defenses are well-known but easy to bypass when frameworks are misused.
XSS — Cross-Site Scripting
Attacker injects script that runs in another user’s browser, in your site’s origin. From the browser’s POV, the script is yours: it can read cookies (unless HttpOnly), DOM, localStorage, and call any same-origin API.
Three flavors
| Type | Injection vector | Example |
|---|---|---|
| Stored | Script saved in DB, served to all users | Comment field that renders raw HTML |
| Reflected | Script in URL, reflected in response | ?q=<script>...</script> echoed in search results page |
| DOM-based | JS in the page reads URL/storage and writes DOM | document.body.innerHTML = location.hash.slice(1) |
The fundamental defense: contextual output encoding
Where the data ends up determines what to escape.
<!-- HTML body -->
<p>Hello {{ name }}</p> <!-- escape: < > & " ' -->
<!-- HTML attribute -->
<input value="{{ name }}"> <!-- escape: < > & " ' -->
<!-- JavaScript -->
<script>const u = "{{ name }}"</script> <!-- escape: < > & ' " \ ; (and never raw user input here — use json_script) -->
<!-- URL -->
<a href="/u/{{ name }}"> <!-- urlencode -->
Framework defaults
- Jinja2 — autoescape ON for
.html/.htm/.xmltemplates by default.{{ var }}HTML-escapes.{{ var | safe }}opts out — never on user data. - Django templates — autoescape ON.
{% autoescape off %}opts out. - React JSX —
{value}escapes.dangerouslySetInnerHTML={{__html: ...}}opts out (the name is a warning — don’t pass user data). - FastAPI + Jinja — same as Jinja above.
# Django: passing data to inline JS — use json_script
{% load json_script %}
{{ user_data | json_script:"data" }}
<script>const data = JSON.parse(document.getElementById("data").textContent);</script>
Never embed Python strings directly into a <script> tag — context-encoding for JS is its own minefield.
Content Security Policy
A second line of defense — even if XSS slips through, CSP can prevent script execution.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{{ nonce }}'; object-src 'none'; base-uri 'self'
script-src 'self'— scripts only from same origin.'nonce-{{ random }}'— inline scripts allowed only if<script nonce="...">matches.'unsafe-inline'— disable CSP’s main XSS protection. Avoid.- Report-only mode:
Content-Security-Policy-Report-Onlyplusreport-urito canary new policies.
In FastAPI:
@app.middleware("http")
async def csp(request, call_next):
resp = await call_next(request)
resp.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self'; "
"object-src 'none'; "
"base-uri 'self'; "
"frame-ancestors 'none'"
)
return resp
HttpOnly + Secure cookies
response.set_cookie(
"session", token,
httponly=True, # JS can't read — XSS can't steal session
secure=True, # HTTPS only
samesite="lax", # CSRF defense (see below)
)
HttpOnly doesn’t prevent XSS but limits damage — XSS can still call APIs as the user but can’t exfiltrate the session token directly.
Sanitization (when raw HTML is required)
If you must accept HTML (e.g., user-formatted comments), allowlist tags/attributes — don’t blocklist.
import bleach
clean = bleach.clean(
user_html,
tags={"p", "b", "i", "a", "ul", "li", "code"},
attributes={"a": ["href", "title"]},
protocols={"http", "https", "mailto"},
)
html-sanitizer is another option; both wrap a parsed HTML tree.
CSRF — Cross-Site Request Forgery
Attacker tricks the user’s browser into making a request to your site, riding the user’s existing session cookie.
<!-- attacker's site -->
<form action="https://yourbank.com/transfer" method="POST">
<input name="to" value="attacker">
<input name="amount" value="1000000">
</form>
<script>document.forms[0].submit()</script>
If the user is logged into yourbank.com, the cookie ships with the request, and the transfer happens.
Defenses
SameSite cookies — modern default protection.
response.set_cookie("session", token, samesite="lax")
# "lax" — sent on top-level GET nav, not on cross-site POST
# "strict" — never sent cross-site (breaks login flows starting from external links)
# "none" — sent always; requires Secure
SameSite=Lax is the default in modern Chrome / Firefox / Edge. Most CSRF attacks die here. But:
- Older browsers don’t enforce it.
- Login flows that need cross-site requests (OAuth callbacks) need
none+Secure.
CSRF tokens — for defense-in-depth or when SameSite isn’t enough:
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
...
</form>
Server generates a per-session (or per-form) random token, embeds in the page, and validates on POST. Attacker’s cross-site request can’t read your origin’s pages, so can’t get the token.
- Django: built-in via
CsrfViewMiddlewareand{% csrf_token %}. - Flask:
flask-wtfforCSRFProtect. - FastAPI: no built-in; use
fastapi-csrf-protector wire your own.
Double-submit cookie — token in cookie + same token as hidden form field. Server compares. Slightly weaker than per-session tokens but stateless.
When CSRF doesn’t apply
Token-authenticated APIs (Authorization: Bearer <jwt> header) are CSRF-immune by construction — browsers don’t auto-attach Authorization headers cross-origin (unlike cookies). If your API is fully token-based, no CSRF tokens needed.
The catch: if you accept both cookie auth and bearer auth on the same endpoint, you still need CSRF tokens for the cookie path.
CSRF + JSON
JSON POSTs aren’t application/x-www-form-urlencoded, so cross-site <form> submissions can’t generate them by default. But fetch from a malicious origin can — modern apps need CSRF protection on JSON endpoints too unless they’re bearer-auth’d.
Browser’s CORS preflight provides some protection: a JSON Content-Type: application/json triggers preflight, and unless your CORS policy allows the attacker origin, the request never goes through. Don’t rely on this alone.
Interview angle
- Q: “What’s XSS and how do you prevent it?” — script in your origin via injected content; output encoding contextually, CSP, HttpOnly cookies.
- Q: “Difference between stored, reflected, and DOM XSS?” — where the script comes from / is rendered.
- Follow-up: “What’s CSRF and what defenses are most effective?” — riding the user’s session cookie; SameSite cookies (default), CSRF tokens (defense-in-depth).
- Follow-up: “Are token-auth APIs CSRF-vulnerable?” — no, browsers don’t auto-attach
Authorizationheaders cross-origin.