Intermediate to senior

Frontend Interview Prep

Fourteen chapters on HTML and CSS, core JavaScript, the event loop, browser rendering, React, state, performance, accessibility, security, TypeScript, testing, machine-coding components and frontend system design, with tested code.

Chapter 10 of 14Quality and security · Web Security for Frontend Engineers

Web Security for Frontend Engineers

Security questions probe whether you understand how the browser isolates, trusts and leaks data. The recurring topics are XSS, CSRF, CORS, the same-origin policy, content security policy, cookies and token storage, and supply-chain risk. This chapter explains each attack, why it works and how to defend, so you can reason about a new scenario rather than recite a list.

1. The same-origin policy

An origin is the triple scheme + host + port. Scripts from one origin cannot read responses or DOM from another origin by default. This isolation is the foundation: it stops a malicious page from reading your bank's page in another tab.

URLSame origin as https://app.example.com?
https://app.example.com/otheryes
http://app.example.comno (scheme)
https://api.example.comno (host)
https://app.example.com:8443no (port)
const sameOrigin = (a, b) => new URL(a).origin === new URL(b).origin;
assert.ok(sameOrigin("https://app.example.com/x", "https://app.example.com/y?q=1"));
assert.ok(!sameOrigin("https://app.example.com", "http://app.example.com"));
assert.ok(!sameOrigin("https://app.example.com", "https://api.example.com"));
assert.ok(!sameOrigin("https://app.example.com", "https://app.example.com:8443"));

The policy restricts reading. Cross-origin sending (a form post, an image request, a script tag) is still allowed, which is why CSRF exists.

2. Cross-site scripting (XSS)

XSS means an attacker gets their JavaScript to run in your page's origin. It can then read anything the page can, call your APIs as the user, steal tokens in storage, and rewrite the page.

TypeHow the payload arrives
Storedsaved by your server (a comment, profile) and served to other users
Reflectedechoed from the request (a search parameter in an error page)
DOM-basedclient code writes untrusted data into the DOM (innerHTML, document.write, location, eval)

Defences

  1. Escape output for its context. HTML text, attribute values, URLs, JavaScript and CSS need different escaping. Modern frameworks (React, Vue, Angular) escape interpolated text by default.
  2. Avoid dangerous sinks: innerHTML, outerHTML, insertAdjacentHTML, document.write, eval, new Function, setTimeout("string"), and React's dangerouslySetInnerHTML. Use textContent.
  3. Sanitise HTML you must render (a rich-text comment) with a vetted library such as DOMPurify, allow-listing tags and attributes. Never write your own regex sanitiser.
  4. Validate URLs: reject javascript: and data: URLs in links the user controls (href, src).
  5. Content Security Policy as defence in depth (below).
  6. HttpOnly cookies keep session cookies out of reach of scripts.
  7. Trusted Types (in supporting browsers) forces dangerous sinks to take vetted values.
const escapeHtml = s => String(s).replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
assert.equal(escapeHtml(`<img src=x onerror="alert(1)">`), "&lt;img src=x onerror=&quot;alert(1)&quot;&gt;");
assert.equal(escapeHtml("Tom & Jerry"), "Tom &amp; Jerry");

function safeHref(url) {
  try {
    const u = new URL(url, "https://example.com");
    return ["http:", "https:", "mailto:"].includes(u.protocol) ? u.href : "#";
  } catch { return "#"; }
}
assert.equal(safeHref("https://example.org/a"), "https://example.org/a");
assert.equal(safeHref("javascript:alert(1)"), "#");                   // the classic link-based XSS
assert.equal(safeHref("  JaVaScRiPt:alert(1)"), "#");                  // case and whitespace tricks are normalised by URL parsing
assert.equal(safeHref("/relative/path"), "https://example.com/relative/path");

Content Security Policy (CSP)

A response header that tells the browser which sources of scripts, styles, images and connections are allowed:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

A strict CSP (nonces or hashes, no unsafe-inline, no unsafe-eval) blocks injected inline scripts even when an XSS bug exists. Roll it out with Content-Security-Policy-Report-Only first. frame-ancestors also controls who may embed your page (clickjacking defence).

3. Cross-site request forgery (CSRF)

If the browser automatically attaches cookies to requests to your site, then a malicious page can make the victim's browser send a state-changing request (a form post to bank.com/transfer) and the request arrives authenticated. The attacker cannot read the response, but the action happens.

Defences

  • SameSite cookies: SameSite=Lax (the default in modern browsers) withholds cookies on cross-site subrequests and non-top-level POSTs; Strict withholds them on all cross-site navigation too; None (requires Secure) allows them everywhere.
  • CSRF tokens: a secret, per-session or per-request value that the page includes in the request (header or hidden field) and the server verifies. A cross-site attacker cannot read it.
  • Double-submit cookie and custom headers on API calls (a cross-site simple form cannot set arbitrary headers).
  • Check Origin / Referer headers on state-changing requests.
  • Never change state with GET.
  • Re-authenticate for sensitive actions.

APIs that authenticate with an Authorization: Bearer header (not cookies) are not automatically CSRF-able, because the browser does not attach that header on its own.

4. CORS: what it is and is not

Cross-Origin Resource Sharing is a way for a server to relax the same-origin policy for specific origins. It is enforced by the browser, protecting users; it does not protect your server from direct requests (curl ignores it).

  • A simple request (GET/HEAD/POST with basic headers) is sent immediately; the browser then checks the response header Access-Control-Allow-Origin before letting your script read it.
  • A preflight OPTIONS request is sent first for "non-simple" requests (custom headers such as Authorization, methods like PUT or DELETE, JSON content type). The server must answer with Access-Control-Allow-Origin, Access-Control-Allow-Methods and Access-Control-Allow-Headers; Access-Control-Max-Age caches the answer.
  • For credentialed requests (cookies, with fetch(..., { credentials: "include" })) the server must name the exact origin (not *) and send Access-Control-Allow-Credentials: true.
// a tiny model of the server side decision for an allow-list of origins
function corsHeaders(origin, allowed) {
  if (!allowed.includes(origin)) return {};                        // no headers: the browser blocks the read
  return { "Access-Control-Allow-Origin": origin, "Vary": "Origin" };
}
const allowed = ["https://app.example.com"];
assert.deepEqual(corsHeaders("https://app.example.com", allowed), { "Access-Control-Allow-Origin": "https://app.example.com", Vary: "Origin" });
assert.deepEqual(corsHeaders("https://evil.example", allowed), {});

Common misunderstandings: "CORS blocks the request" (often the request still reached the server; the browser blocks your script from reading the response), "* with credentials works" (it does not), "fix CORS in the frontend" (you cannot; the server must send the headers, or you proxy through your own origin). Never reflect any Origin back blindly: that defeats the purpose.

5. Where to store tokens and sessions

StorageXSS can read it?CSRF riskNotes
localStorage / sessionStorageyesno (not auto-sent)any XSS steals the token
In-memory variableyes while running, but short-livednolost on refresh; pair with a refresh mechanism
HttpOnly, Secure, SameSite cookieno (script cannot read it)yes, needs SameSite and CSRF defencesthe usual recommendation for session credentials

No option is perfect. A common robust design: a short-lived access token held in memory, plus a long-lived refresh token in an HttpOnly; Secure; SameSite cookie, with strict CSP to reduce XSS. Never put secrets (API keys, private tokens) in frontend code: everything shipped to the browser is public.

// cookie flags worth setting on a session cookie
const sessionCookie = "sid=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600";
const flags = Object.fromEntries(sessionCookie.split("; ").slice(1).map(p => p.split("=")).map(([k, v]) => [k, v ?? true]));
assert.equal(flags.HttpOnly, true);
assert.equal(flags.Secure, true);
assert.equal(flags.SameSite, "Lax");

6. Clickjacking

An attacker embeds your page in an invisible iframe and tricks the user into clicking. Defend with Content-Security-Policy: frame-ancestors 'none' (or a list of allowed parents) and the older X-Frame-Options.

7. Other frontend risks

  • Open redirects: ?next=https://evil.com followed blindly after login. Only redirect to relative paths or an allow-list.
  • Third-party scripts and supply chain: every analytics tag, widget and npm package runs with your page's privileges. Pin and audit dependencies (npm audit, lockfiles, provenance), minimise third parties, use Subresource Integrity (integrity="sha384-...") for CDN scripts, and sandbox untrusted iframes (sandbox attribute).
  • postMessage: always check event.origin and validate the message shape; specify the target origin when sending.
  • target="_blank": modern browsers imply noopener, but add rel="noopener noreferrer" for older ones.
  • Sensitive data exposure: do not log tokens, avoid putting secrets in URLs (they leak through history, logs and Referer), and avoid caching authenticated responses on shared devices.
  • Prototype pollution: merging untrusted JSON into objects with __proto__ keys can change behaviour globally. Use safe merge utilities, Object.create(null) maps, or Map.
  • ReDoS: nested quantifiers in regular expressions can freeze the main thread on crafted input.
  • Input validation and output encoding: validate shape on the server; the client's validation is only UX.
function safeRedirectTarget(next, fallback = "/") {
  if (typeof next !== "string") return fallback;
  if (!next.startsWith("/") || next.startsWith("//") || next.startsWith("/\\")) return fallback;   // reject protocol-relative URLs
  return next;
}
assert.equal(safeRedirectTarget("/account"), "/account");
assert.equal(safeRedirectTarget("https://evil.com"), "/");
assert.equal(safeRedirectTarget("//evil.com/phish"), "/");
assert.equal(safeRedirectTarget(undefined), "/");

8. HTTPS and headers checklist

  • HTTPS everywhere, with HSTS (Strict-Transport-Security) so browsers refuse plain HTTP.
  • CSP, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, frame-ancestors.
  • Cookies: HttpOnly, Secure, SameSite, scoped Path and Domain.
  • Cache-Control: no-store on sensitive responses.

9. Answering "how would you secure this app?"

  1. Authentication and sessions: HttpOnly Secure SameSite cookies, short lifetimes, rotation.
  2. Input and output: escape by default, sanitise rich text, avoid dangerous sinks.
  3. CSRF: SameSite plus tokens for cookie-authenticated state changes.
  4. CORS: a strict allow-list, no wildcard with credentials.
  5. CSP and security headers.
  6. Dependencies and third parties: audits, SRI, minimal scripts.
  7. Secrets: none in the client; proper server-side handling.
  8. Monitoring: CSP reports, error tracking, rate limiting on the server.

10. Common mistakes

  • innerHTML with user data and dangerouslySetInnerHTML without sanitising.
  • Storing long-lived tokens in localStorage without any XSS mitigation.
  • Thinking CORS is a security feature of the server, or that disabling it "fixes" a request.
  • Access-Control-Allow-Origin: * on authenticated endpoints, or reflecting arbitrary origins.
  • No CSRF protection on cookie-authenticated POST endpoints.
  • Putting secrets in frontend bundles or environment variables exposed to the client.
  • Trusting client-side validation or client-side authorisation checks.
  • Unreviewed third-party scripts.

11. Practice questions

  1. What is the same-origin policy? Which things does it block and which does it not?
  2. Explain stored, reflected and DOM-based XSS and the main defences.
  3. How does CSRF work, and how do SameSite cookies and tokens stop it?
  4. Walk through a CORS preflight. Why does * fail with credentials?
  5. localStorage versus HttpOnly cookies for tokens: trade-offs?
  6. What does a Content Security Policy do and how would you deploy one safely?
  7. How do you safely render user-provided HTML?
  8. What risks do third-party scripts bring, and how do you mitigate them?
Header Logo