Frontend Security

XSS, CSRF, clickjacking, token storage, CORS and supply-chain risks, explained with pictures. What can go wrong in the browser and how to defend against it.

Intermediate⏱ 5 min readLesson 9 of 11#frontend#security#xss#csrf#cors#csp

The big idea

The browser is a shared building: your app, third-party scripts, browser extensions and other websites all live in it. Browser security is about making sure the wrong code can't read or act on your users' data.

Common frontend attacks at a glanceCommon frontend attacks at a glance

1. XSS: Cross-Site Scripting ⭐

An attacker gets their JavaScript to run on your page, so it can steal tokens, read the page, or act as the user.

Drawing diagram…

How React protects you (mostly)

React escapes everything you put in {}: <script> becomes harmless text.

const comment = '<img src=x onerror="alert(1)">';
<p>{comment}</p>   // ✅ shown as text, not executed

Where XSS still sneaks in

// ❌ 1. dangerouslySetInnerHTML with user content
<div dangerouslySetInnerHTML={{ __html: userBio }} />

// ✅ Sanitise first
import DOMPurify from "dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userBio) }} />

// ❌ 2. javascript: URLs
<a href={user.website}>Website</a>   // user.website = "javascript:steal()"

// ✅ Allow only http(s)
const safeUrl = /^https?:\/\//i.test(user.website) ? user.website : "#";

// ❌ 3. Direct DOM APIs
element.innerHTML = userInput;
// ✅
element.textContent = userInput;

Content Security Policy (CSP): the safety net

A CSP header tells the browser which scripts are allowed to run. Even if an attacker injects a <script>, the browser refuses to run it.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m';
  img-src 'self' https://cdn.example.com data:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';

2. CSRF: Cross-Site Request Forgery

You're logged into your bank. You visit an evil site, which silently submits a form to the bank. Your browser automatically attaches your bank cookie, so the bank thinks you made the transfer.

Drawing diagram…

Defences:

DefenceHow it helps
SameSite=Lax or Strict cookiesThe browser doesn't send the cookie on cross-site POSTs (the default in modern browsers is Lax)
CSRF tokensA secret value in the form that evil.com can't know
Check the Origin headerReject requests from unexpected origins
Tokens in headers instead of cookiesAuthorization: Bearer … isn't sent automatically

3. Where to store tokens?

StorageXSS can steal it?CSRF risk?Verdict
localStorage / sessionStorage❌ Yes: any script can read itNoAvoid for sensitive tokens
JS memory (a variable)Harder (lost on refresh)NoOK for short-lived access tokens
HttpOnly + Secure + SameSite cookie✅ No: JS can't read itMitigated by SameSite✅ Recommended for sessions / refresh tokens
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400

💡 A popular pattern is the BFF (Backend for Frontend): the browser talks only to your own server with an HttpOnly session cookie, and that server holds the real API tokens. Tokens never touch browser JavaScript.

4. CORS: often misunderstood

The Same-Origin Policy stops JavaScript on evil.com from reading responses from api.bank.com. CORS is how a server relaxes that rule for origins it trusts.

Drawing diagram…
  • CORS is enforced by the browser. It doesn't protect your API from curl or Postman; it protects your users from other websites.
  • ❌ Never combine Access-Control-Allow-Origin: * with credentials, and never blindly reflect whatever Origin arrives.
  • ✅ Allow-list exact origins.

5. Clickjacking

An evil page loads your site in an invisible iframe on top of a fake "Win a prize!" button. The user clicks "prize" but actually clicks "Delete account" on your site.

Defence: forbid framing.

Content-Security-Policy: frame-ancestors 'none';
X-Frame-Options: DENY

6. Supply chain: your dependencies

A typical frontend app has 1,000+ npm packages. One compromised package can steal data from every site that uses it.

Drawing diagram…
  • Commit your lockfile and use npm ci in CI.
  • Run npm audit, Dependabot or Snyk.
  • Prefer fewer, well-maintained dependencies.
  • Load third-party scripts with Subresource Integrity (SRI):
<script src="https://cdn.example.com/lib.min.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>

7. Never trust the frontend

⚠️ Everything in the browser can be changed by the user. Hidden form fields, disabled buttons, client-side validation, prices in JavaScript, "isAdmin" flags: all of it can be edited in DevTools.

Frontend validation is for user experience. The backend must validate and authorise everything again. And never put secrets (API keys with write access, database passwords) in frontend code: they end up in the bundle for anyone to read.

Security headers checklist

HeaderPurpose
Content-Security-PolicyLimit where scripts, styles and frames can come from
Strict-Transport-SecurityForce HTTPS
X-Content-Type-Options: nosniffStop MIME-type guessing
Referrer-Policy: strict-origin-when-cross-originDon't leak full URLs
Permissions-PolicyDisable camera, microphone and geolocation unless needed

Key takeaways

  • XSS: never inject unsanitised HTML; avoid dangerouslySetInnerHTML and javascript: URLs; add a CSP.
  • CSRF: use SameSite cookies, CSRF tokens and Origin checks.
  • Store session tokens in HttpOnly, Secure, SameSite cookies, not localStorage.
  • CORS relaxes the same-origin policy; allow-list exact origins.
  • Block clickjacking with frame-ancestors; audit dependencies; never trust the client.