interviewDeck

Your one-stop platform to prepare, practice and ace your interviews.

Loading your questions…

All Questions

Filters & tools

Web Security Interview Questions and Answers

22 hand-picked Web Security interview questions with detailed answers. Open the interactive version above to search, filter by difficulty, run code, bookmark questions and track your progress.

What is XSS and how do you prevent it?

Cross-Site Scripting: attacker-controlled data is interpreted as code in a victim's browser, running with your site's origin — so it can read cookies (unless HttpOnly), call your APIs as the user, and rewrite the page.

Three types:

  • Stored — the payload is persisted (a comment, a profile field) and served to every viewer. Most damaging.
  • Reflected — the payload comes from the request (a query param echoed into the page) and needs a crafted link.
  • DOM-based — never touches the server: client JS takes a value from location.hash/localStorage and writes it into a dangerous sink.

Prevention: escape on output, contextually (HTML, attribute, URL and JS contexts each need different escaping). Modern frameworks escape interpolated values by default — the vulnerabilities live in the escape hatches: innerHTML, dangerouslySetInnerHTML, [innerHTML], v-html, and building URLs from user input. Sanitise any HTML you must render with a maintained library (DOMPurify), and layer a strict CSP as defence in depth.

// dangerous sinks — every XSS bug in a modern app is near one of these
el.innerHTML = userInput;                    // ✗
el.outerHTML = userInput;                    // ✗
document.write(userInput);                   // ✗
eval(userInput); new Function(userInput);    // ✗
location.href = userInput;                   // ✗ javascript: URLs

// safe
el.textContent = userInput;                  // ✓ never parsed as HTML
el.innerHTML = DOMPurify.sanitize(userHtml); // ✓ when HTML is required

How does Content Security Policy work, and how do you deploy one?

A response header telling the browser which sources are allowed to load and execute. Even if an XSS payload lands, a good CSP stops it running — it's the main defence-in-depth layer for XSS.

The important part is script control. An allowlist of domains is weak (one CDN hosting a vulnerable library or JSONP endpoint bypasses it). The modern approach is nonce- or hash-based with strict-dynamic: only scripts carrying the per-response nonce run, and scripts they load inherit trust.

Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https:;
  object-src 'none'; base-uri 'none'; frame-ancestors 'none'

Deployment: ship Content-Security-Policy-Report-Only with a report-uri/report-to first, collect violations for a few weeks, fix inline scripts and styles, then enforce. Going straight to enforce breaks production.

What is CSRF and what actually prevents it?

Cross-Site Request Forgery: a malicious site causes the victim's browser to send an authenticated request to yours. It works because cookies are attached automatically by origin, so the server can't tell the request wasn't initiated by your UI. The attacker never reads the response — they only need the side effect (transfer money, change email).

Defences:

  • SameSite cookiesLax (now the browser default) blocks cross-site POSTs; Strict blocks even top-level GET navigation. Strong, but not a complete substitute: it doesn't protect against same-site attackers (a vulnerable subdomain) and behaviour varies.
  • Anti-CSRF tokens — a per-session/per-request token the attacker can't read due to the same-origin policy. Synchroniser token (server-stored) or double-submit cookie.
  • Origin/Referer validation on state-changing requests.
  • Custom header requirement — a header like X-Requested-With forces a CORS preflight, which a cross-site form can't produce.

Note that token-in-header auth (Authorization: Bearer) is not CSRF-prone — nothing is sent automatically. CSRF is fundamentally a cookie-auth problem.

What does CORS actually do, and what are the common mistakes?

The Same-Origin Policy blocks a page from reading responses from another origin. CORS is the server's way of opting in — it does not add security, it relaxes it in a controlled way.

Simple vs preflighted: GET/POST/HEAD with a small set of "safe" headers and content types go straight through (the response is just hidden if not allowed). Anything else — PUT, DELETE, Content-Type: application/json, custom headers — triggers an OPTIONS preflight first.

Mistakes that matter:

  • Access-Control-Allow-Origin: * together with credentials — the browser rejects it, and reflecting the caller's Origin instead effectively allows everyone.
  • Thinking CORS protects the API. It doesn't: the request still reaches your server, and non-browser clients ignore CORS entirely. Authentication and authorisation are what protect the API.
  • Forgetting Vary: Origin, which lets a CDN cache one origin's CORS response for another.
  • Not handling the preflight (returning 401 for OPTIONS, which carries no credentials).

Where should you store auth tokens in a browser?

The honest answer is a trade-off between two attack classes, not a single winner:

  • localStorage / sessionStorage — readable by any JavaScript on the page, so any XSS steals the token. No CSRF exposure (nothing is sent automatically). Convenient for calling APIs on other origins.
  • HttpOnly, Secure, SameSite cookie — invisible to JavaScript, so XSS can't read it (though it can still use the session by making requests). Introduces CSRF exposure, which SameSite plus tokens handles.

The generally recommended shape: refresh token in an HttpOnly; Secure; SameSite=Strict cookie scoped to the auth endpoint, short-lived access token held in memory only (a JS variable, not storage), refreshed silently. XSS can then only steal a token that expires in minutes, and nothing survives a tab close.

Whatever you choose: keep tokens short-lived, support revocation server-side, and remember that if you have XSS you have a compromise regardless of storage.

What are the security pitfalls of JWTs?

  • Base64 is not encryption. Anyone can decode the payload — never put secrets or PII in a JWT.
  • Algorithm confusion — accepting alg: none, or letting an attacker switch RS256 to HS256 and sign with the public key as the HMAC secret. Always pin the expected algorithm server-side; never trust the header.
  • Revocation — a stateless JWT is valid until it expires. Logout, ban and password change don't invalidate it without a denylist or short expiry, which partly gives up the statelessness that motivated JWTs.
  • Long expiry — a 30-day access token is a 30-day breach. Minutes for access, longer for refresh, with rotation and reuse detection.
  • Missing claim validation — verify exp, nbf, iss, aud. A token issued for another service being accepted by yours is a real and common bug.
  • Weak HMAC secrets — brute-forceable offline. Use a long random secret, or asymmetric keys.

How should a SPA implement OAuth 2 login?

Authorization Code flow with PKCE — the only currently recommended flow for public clients (SPAs and mobile apps).

  1. The client generates a random code_verifier and sends its SHA-256 hash as code_challenge with the authorize request.
  2. The user authenticates; the provider redirects back with a one-time code.
  3. The client exchanges the code plus the original verifier for tokens.

PKCE exists because a SPA cannot hold a client secret — anything in the bundle is public. Without it, an attacker who intercepts the redirect (a malicious app registered for the same URI, a leaked referrer) could redeem the code. With it, the code is useless without the verifier that never left the client.

Also required: the state parameter (CSRF protection on the callback), nonce for OIDC ID tokens, exact-match redirect URIs, and validating the ID token signature, issuer, audience and expiry. The implicit flow is deprecated — it returned tokens in the URL fragment, where they leak into history, logs and referrers.

Explain the security attributes of a cookie.

  • HttpOnly — not readable by document.cookie, so XSS can't exfiltrate it. Mandatory for session cookies.
  • Secure — only sent over HTTPS, preventing network interception.
  • SameSiteStrict (never sent cross-site), Lax (sent on top-level GET navigation; the modern default), None (sent always, and requires Secure).
  • Domain — omit it to scope the cookie to the exact host. Setting .example.com shares it with every subdomain, so one compromised subdomain gets your session cookie.
  • Path — weak isolation; not a security boundary.
  • Max-Age/Expires — session cookie (cleared on browser close) versus persistent.
  • Prefixes__Host- forces Secure, no Domain and Path=/, which is the strongest binding available; __Secure- forces Secure.

Which security headers should every app send?

  • Strict-Transport-Security (HSTS) — forces HTTPS for a given period, defeating SSL-strip downgrade attacks. max-age=31536000; includeSubDomains (preload once you're sure).
  • Content-Security-Policy — the main XSS mitigation.
  • X-Content-Type-Options: nosniff — stops the browser guessing a content type and executing an uploaded file as script.
  • Referrer-Policystrict-origin-when-cross-origin keeps paths and query strings (often containing tokens or IDs) out of third-party referrers.
  • frame-ancestors in CSP (superseding X-Frame-Options) — anti-clickjacking.
  • Permissions-Policy — disable camera, microphone, geolocation and payment APIs you don't use, including for embedded iframes.
  • Cross-origin isolationCOOP/COEP/CORP against Spectre-style cross-origin leaks, and required for SharedArrayBuffer.

They're cheap: mostly a few lines of CDN or server config, and they measurably reduce impact when something else fails.

What is clickjacking and how do you defend against it?

The attacker loads your page in a transparent iframe over their own UI, so the victim thinks they're clicking "Play video" while actually clicking "Delete account" or "Approve payment" on your site — with their session attached.

Defence: tell the browser who may frame you.

  • Content-Security-Policy: frame-ancestors 'none' (or an explicit allowlist) — the modern control, and the one that supports multiple origins.
  • X-Frame-Options: DENY | SAMEORIGIN — the legacy header; still worth sending for old browsers.

Frame-busting JavaScript is not a defence — sandbox on the attacker's iframe disables scripts.

Related: for genuinely sensitive actions add a second factor the overlay can't fake — re-authentication, a typed confirmation, or a randomly positioned confirm step.

How do you reduce supply-chain risk in a frontend project?

Your app runs whatever your 1,200 transitive dependencies run — plus whatever your build scripts do.

  • Lockfile committed, and npm ci in CI so builds are reproducible and a range doesn't silently pull a new version.
  • Audit and patchnpm audit, Dependabot/Renovate, with a policy for how fast criticals get merged.
  • Fewer dependencies — the most effective control. Question every package that wraps ten lines of code.
  • Beware typosquats and hijacked packages — check downloads, repo, maintainers and last publish before adding something new.
  • Disable install scripts where practical (--ignore-scripts); postinstall is the classic malware vector.
  • Pin and verify third-party scripts loaded at runtime with Subresource Integrity — a hash so a compromised CDN can't swap the file (this is the Magecart attack).
  • Generate an SBOM and scan builds if you're in an enterprise context.

Why can't you keep a secret in frontend code?

Everything shipped to the browser is readable: bundles, source maps, environment variables inlined at build time, and every network request in DevTools. Minification is not obfuscation, and obfuscation is not protection.

So these are all public: API keys baked into the bundle, NEXT_PUBLIC_*/VITE_* variables (the prefix literally means "exposed"), values fetched from a config endpoint, and anything in a JWT payload.

What to do instead:

  • Keep secrets on a server and proxy the call — the browser talks to your backend, your backend holds the key.
  • For keys that must be public (Maps, analytics, Firebase web config), restrict them at the provider: HTTP referrer allowlists, per-key scopes, quotas, and server-side rules.
  • Enforce authorisation server-side — hiding a button is UX, not access control.

Is client-side validation a security control?

No. It's a user-experience feature — instant feedback, fewer round trips, clearer forms. An attacker simply uses curl, Postman, or DevTools to edit the request, and none of your JavaScript runs.

The server must independently validate: types and formats, ranges and lengths, allowed enum values, business rules (can this user perform this action on this resource?), and object ownership.

Two related traps:

  • Mass assignment — binding the whole request body to a model lets a caller set role: "admin". Use explicit allowlists of writable fields.
  • Hidden or disabled fields — a disabled input or a hidden price field is fully editable by the client. Never trust a value just because your UI didn't offer a way to change it.

Where it's practical, share one schema (Zod/Yup/JSON Schema) between client and server so the rules can't drift — but the server still enforces.

You must render user-supplied HTML. How do you do it safely?

Escaping isn't an option (you need the markup to render), so you must sanitize — parse the HTML and remove anything dangerous — with a maintained library, never a regex or blocklist.

DOMPurify is the standard choice. Configure an allowlist of tags and attributes rather than trying to enumerate what's bad, and remember the attack surface is wider than <script>: event handler attributes (onerror, onload), javascript: URLs in href/src, <iframe>, <object>, SVG (which can carry script), and CSS expressions.

Where to sanitize: on output/render, not only on input — sanitising on save leaves any pre-existing stored data unsafe and breaks if the rules change. Doing both is fine; doing only the input side is the common mistake.

Extra layers: a strict CSP so anything that slips through can't execute, and Trusted Types (Chromium) which makes assigning a raw string to innerHTML a runtime error unless it passed a policy.

const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['p','b','i','em','strong','a','ul','ol','li','code','pre','br'],
  ALLOWED_ATTR: ['href','title'],
  ALLOWED_URI_REGEXP: /^(?:https?|mailto):/i   // blocks javascript: and data:
});
el.innerHTML = clean;

What is an open redirect and why does it matter?

Your app takes a URL from a parameter (?returnUrl=, ?next=) and redirects to it without validation. An attacker sends https://yourbank.com/login?next=https://evil.com — the link genuinely starts on your trusted domain, so it survives user scrutiny, email filters and link scanners, then lands the victim on a phishing page.

It's also a component in bigger attacks: OAuth token theft via a manipulated redirect URI, and bypassing allowlists in SSRF chains.

Prevention:

  • Prefer relative paths only — reject anything with a scheme or host.
  • If absolute URLs are genuinely needed, use a strict allowlist of hosts.
  • Better still, don't put the destination in the URL: store it server-side against the session, or map an opaque key to a known route.
  • Watch the bypass tricks: //evil.com (protocol-relative), https:/\evil.com, encoded characters, and @-in-authority tricks. Parse with the URL API and compare the resolved origin rather than string-matching.

What are the security rules for postMessage and iframes?

postMessage deliberately crosses the origin boundary, so both ends must do the checking the browser no longer does for you.

Sending: always specify the exact target origin. iframe.contentWindow.postMessage(data, '*') broadcasts to whatever document is loaded there — which may not be the one you think after a redirect.

Receiving: validate event.origin against an allowlist first, before touching event.data; then validate the shape of the data. A handler that does eval(event.data) or writes it into innerHTML is an XSS entry point from any site that can open a window to yours.

Embedding untrusted content: use sandbox and grant back only what's needed. sandbox="allow-scripts allow-same-origin" together defeats the point — the frame can reach into its own origin and remove the sandbox.

Being embedded: control it with CSP frame-ancestors.

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted.example.com') return;   // FIRST
  const msg = event.data;
  if (typeof msg !== 'object' || msg.type !== 'RESIZE') return; // then shape
  setHeight(Number(msg.height) || 0);
});

What are the security concerns with file uploads?

  • Type checking must be server-side and content-based — the extension and the client-sent MIME type are attacker-controlled. Sniff the magic bytes, and for images re-encode them (which also strips EXIF and any embedded payload).
  • Never serve uploads from your app's origin — an uploaded HTML or SVG file becomes stored XSS with full access to your session. Use a separate domain or a storage bucket, and send Content-Disposition: attachment plus X-Content-Type-Options: nosniff.
  • Path traversal — never use the client-supplied filename to build a path. Generate your own identifier.
  • Size and rate limits — otherwise it's a cheap denial-of-service and a storage bill.
  • Malware scanning for anything other users will download.
  • Authorisation on download — unguessable URLs are not access control. Use signed, expiring URLs.
  • Archive bombs if you unpack anything server-side.

Session cookies vs token-based auth — which do you choose?

Server sessions (cookie holds an opaque id): state lives server-side, so revocation is instant and the cookie reveals nothing. Needs a shared session store when you scale horizontally, and it's cookie-based so CSRF applies. Excellent default for a first-party web app.

Tokens (JWT in a header): stateless verification — any service can validate the signature without a lookup, which suits microservices, mobile clients and third-party APIs. Costs you revocation (valid until expiry) and puts the token where JavaScript can reach it unless you use cookies anyway.

How to decide: a first-party web app with one backend → sessions. Multiple services, mobile apps or external API consumers → tokens, with short expiry plus refresh rotation. Many teams end up with the BFF pattern: the browser gets a session cookie, the backend holds tokens and talks to downstream services.

Which OWASP Top 10 risks show up in frontend work?

  • Broken Access Control — hiding a route or a button in the client while the API still serves the data to anyone who asks. The most common real vulnerability, and it's an API bug the frontend makes easy to spot.
  • Injection (XSS) — dangerous DOM sinks and unsanitised HTML.
  • Security Misconfiguration — missing security headers, permissive CORS, source maps and debug endpoints in production.
  • Identification & Authentication Failures — token storage, session fixation, weak logout, no re-auth for sensitive actions.
  • Vulnerable and Outdated Components — the npm dependency tree.
  • Software & Data Integrity Failures — third-party scripts without SRI, compromised build pipelines.
  • Cryptographic Failures — data over HTTP, no HSTS, secrets in the bundle, home-grown "encryption" in JavaScript.

How would you security-review a frontend codebase?

Work from data flow rather than a checklist:

  1. Map the trust boundaries — where does untrusted data enter (URL params, API responses, postMessage, user input, third-party scripts) and where does it end up?
  2. Grep the dangerous sinksinnerHTML, dangerouslySetInnerHTML, v-html, bypassSecurityTrust*, eval, new Function, document.write, dynamic location assignment. Every hit needs a justification.
  3. Check auth handling — where tokens live, how they're attached, what happens on 401, whether logout actually invalidates, and whether any route guard is the only protection.
  4. Check the API contract — does the client ever receive data the user shouldn't see (over-fetching then filtering client-side is a data leak).
  5. Inspect the response headers in production.
  6. Audit dependencies and third-party scripts, including what the build injects.
  7. Look at what's shipped — source maps, console logs with tokens, commented-out endpoints, test credentials.

What is the Same-Origin Policy?

The browser's foundational isolation rule: a document from one origin cannot read data from another. Origin = scheme + host + port, all three exactly matching — https://app.com and https://api.app.com are different origins, as are http:// and https:// versions of the same host.

What it blocks: reading another origin's DOM (via an iframe or window handle), reading fetch/XHR responses, and reading cross-origin canvas pixel data or detailed script errors.

What it does not block: sending requests. Embedding cross-origin images, scripts, stylesheets and form submissions has always been allowed — which is precisely why CSRF exists.

Controlled escapes: CORS (server opts in to being read), postMessage (explicit messaging), document.domain (deprecated), and JSONP (legacy, avoid).

How do you protect a public-facing form or API from abuse?

Start from the fact that the frontend can't enforce anything — a client-side debounce or a disabled button stops nobody.

  • Server-side rate limiting — per IP, per account and per action, with sensible response codes (429 plus Retry-After).
  • Progressive friction — allow the first few attempts freely, then add delay, then a challenge. Blocking immediately punishes real users who mistyped a password.
  • CAPTCHA / attestation for signup, login and contact forms — modern options (hCaptcha, Turnstile) are mostly invisible to real users.
  • Cost asymmetry — make expensive operations (password hashing, email sending, report generation) require an authenticated, rate-limited path.
  • Enumeration resistance — identical responses and timings for "user exists" and "user doesn't", on login, signup and password reset alike.
  • Monitoring — alert on spikes in failed logins, signups and 4xx rates; abuse shows up as a traffic-shape change first.