Security interview questions
Web application security questions interviewers actually ask: the OWASP Top 10:2025, XSS, CSRF and SameSite, SQL injection, SSRF, broken access control, password storage, sessions versus JWTs, CORS, CSP, secrets, supply-chain attacks, and threat modeling.
Answer out loud or on paper before revealing. If your answer misses a key point, revisit the linked guide or explainer.
Nothing is due right now. Every question in this set is scheduled for a later review.
What is the OWASP Top 10, and what are the main categories in the current edition?
Reveal model answer
The OWASP Top 10 is an awareness document ranking the most critical web application security risk categories, built from contributed testing data and a community survey; the current edition is 2025. In order: A01 Broken Access Control (which now absorbs SSRF), A02 Security Misconfiguration, A03 Software Supply Chain Failures (new, widening the old vulnerable-components category to builds and distribution), A04 Cryptographic Failures, A05 Injection, A06 Insecure Design, A07 Authentication Failures, A08 Software or Data Integrity Failures, A09 Security Logging and Alerting Failures, and A10 Mishandling of Exceptional Conditions (new: error handling, logic errors, failing open). It is a starting point for prioritising, not a complete standard; OWASP ASVS is the verification standard to test against.
WHAT A STRONG ANSWER COVERS
- Knows it is a ranked list of risk categories, not individual bugs.
- Names the 2025 edition and that broken access control is still number one.
- Notes the 2025 changes: supply chain failures and exceptional conditions are new, SSRF folded into access control.
- Treats it as awareness, with ASVS for verification.
RED FLAGS
- Quoting the 2021 list as current, with SSRF as its own category.
- Treating 'we pass the Top 10' as proof an application is secure.
LIKELY FOLLOW-UPS
- Why do you think access control stays at number one?
- What does 'failing open' look like in code?
How did your answer compare?
SOURCES
What are the types of XSS, and how do you defend against them in a React app?
Reveal model answer
XSS is attacker-controlled script running in your origin. Stored XSS persists payloads on the server and serves them to other users, reflected XSS bounces a payload from the request (often a URL parameter) straight into the response, and DOM-based XSS happens entirely in client code when a source like `location.hash` flows into a sink like `innerHTML`, so server-side filters and WAFs never see it. The primary defense is contextual output encoding, which React gives you for text by default; the holes are escape hatches like `dangerouslySetInnerHTML`, `href={userUrl}` accepting `javascript:` URLs, and direct DOM writes. When users must author HTML, sanitize with a maintained library such as DOMPurify, then add a strict CSP and HttpOnly cookies as defense in depth to limit the blast radius.
WHAT A STRONG ANSWER COVERS
- Distinguishes stored, reflected, and DOM-based XSS by where the payload lives and flows.
- Encoding must match the output context (HTML body, attribute, URL, JavaScript).
- Names framework escape hatches and URL schemes as the real React risk.
- Sanitizes rich HTML with DOMPurify rather than hand-written regexes.
- CSP and HttpOnly as layers, not the primary fix.
RED FLAGS
- 'React prevents XSS, so we are safe.'
- Input validation or a blocklist of `<script>` as the main defense.
- Believing HttpOnly stops XSS; it only stops cookie theft via `document.cookie`.
LIKELY FOLLOW-UPS
- Where would you sanitize: on write, on read, or both?
- What is wrong with `<a href={profile.website}>`?
How did your answer compare?
SOURCES
Explain CSRF. Do SameSite cookies make CSRF tokens unnecessary?
Reveal model answer
CSRF tricks a logged-in user's browser into sending a state-changing request to your site from another site, and it works because the browser attaches ambient credentials (cookies) automatically. SameSite helps: `Lax` withholds cookies from cross-site subrequests and non-GET navigations, `Strict` withholds them from all cross-site requests. But OWASP treats SameSite as defense in depth: Chromium defaults unmarked cookies to `Lax` (and still sends them on cross-site top-level POSTs for about two minutes after they are set) while Firefox and Safari do not, `Lax` still allows top-level GET navigations (so state-changing GETs remain exploitable), and same-site is not same-origin, so a compromised sibling subdomain can still attack. Use the framework's built-in protection: a synchronizer token for stateful apps, a signed double-submit cookie for stateless ones, or Fetch Metadata (`Sec-Fetch-Site`) checks with an Origin fallback, and set SameSite explicitly on top.
WHAT A STRONG ANSWER COVERS
- CSRF exploits automatically attached credentials, so it targets cookie-based auth.
- Explains Strict, Lax, and None and that defaults differ across browsers.
- Lax does not protect state-changing GET endpoints.
- Same-site versus same-origin: subdomains are same-site.
- Token patterns or Fetch Metadata as the primary control.
RED FLAGS
- 'We use JSON, so CSRF is impossible' without checking the endpoint rejects form-encoded or text/plain bodies.
- Thinking CORS blocks the request; CORS only blocks reading the response.
- A naive double-submit cookie that is not bound to the session.
LIKELY FOLLOW-UPS
- Is an API that authenticates with a bearer token in a header vulnerable to CSRF?
- How does a subdomain takeover weaken SameSite?
How did your answer compare?
SOURCES
What is wrong with `db.query("SELECT * FROM users WHERE email = '" + email + "' ORDER BY " + sort)`, and how do you fix it?
Reveal model answer
Both values are concatenated into the SQL text, so input like `' OR '1'='1` or a crafted `sort` changes the query's structure: classic SQL injection. Fix the value with a parameterized query, `WHERE email = $1` with `email` bound separately, so the database never parses it as SQL. Placeholders cannot bind identifiers or keywords, so `sort` must be mapped through an allowlist in code (for example `{ newest: 'created_at DESC' }`), rejecting anything unknown. Manual escaping is strongly discouraged because it depends on dialect and server settings, and ORMs are only safe while you use their binding APIs rather than raw string fragments.
WHAT A STRONG ANSWER COVERS
- Parameterized queries or prepared statements as the primary fix.
- Identifiers and ORDER BY direction cannot be parameters; allowlist them.
- Escaping is a last resort, not a defense.
- ORMs and stored procedures are safe only when values are bound.
- Least-privilege database accounts limit the damage.
RED FLAGS
- Escaping quotes or stripping characters as the fix.
- Trying to pass a column name as a bound parameter.
- 'We use an ORM so injection is impossible' while using raw query helpers.
LIKELY FOLLOW-UPS
- How would you safely build a dynamic filter with optional fields?
- What is second-order SQL injection?
How did your answer compare?
SOURCES
Your app fetches a URL the user provides to generate a link preview. What can go wrong, and how do you defend it?
Reveal model answer
That is server-side request forgery: the attacker makes your server request internal targets it can reach and they cannot, such as `http://169.254.169.254/` cloud metadata (credentials), admin panels on localhost, or services on private ranges. Defenses: accept a hostname rather than an arbitrary URL where possible and allowlist destinations; otherwise resolve the name, check every A and AAAA result against blocked ranges (loopback, RFC 1918, link-local, IPv6 unique-local), and connect to that validated IP so DNS rebinding cannot swap the address between check and connect. Disable redirect following (or re-validate every hop), restrict schemes to http and https, and enforce egress at the network level with a dedicated fetcher in a segment that cannot reach internal services. On AWS, require IMDSv2 and disable IMDSv1 as defense in depth.
WHAT A STRONG ANSWER COVERS
- Names cloud metadata and internal services as the real targets.
- Allowlist first; blocklists of IP ranges are bypass-prone fallbacks.
- Resolve, validate, then connect to the same IP to beat DNS rebinding.
- Redirects and alternate IP encodings bypass naive string checks.
- Network egress controls and IMDSv2 as layers.
RED FLAGS
- Checking that the URL string does not contain 'localhost' or '127.0.0.1'.
- Validating the DNS answer, then letting the HTTP client resolve again.
- Following redirects automatically after validating only the first URL.
LIKELY FOLLOW-UPS
- Why did OWASP fold SSRF into broken access control in 2025?
- How would you isolate the fetcher in Kubernetes?
How did your answer compare?
SOURCES
What is wrong with `app.get('/api/invoices/:id', auth, (req, res) => res.json(db.invoices.find(req.params.id)))`?
Reveal model answer
It authenticates the caller but never authorizes the object, so any logged-in user can read any invoice by changing the ID: an insecure direct object reference, the most common form of broken access control (OWASP A01, and BOLA in API terms). Fix it by checking ownership or permission for the specific object on every request, ideally by scoping the query itself, for example `find({ id, tenantId: req.user.tenantId })`, and returning 404 so existence is not leaked. Deny by default, centralise the policy so new routes cannot forget it, and add tests that assert user A cannot read user B's resources. Random UUIDs make IDs harder to guess but are not access control.
WHAT A STRONG ANSWER COVERS
- Distinguishes authentication from authorization.
- Object-level checks on every request, server side.
- Scoping queries by tenant or owner rather than checking afterwards.
- Deny by default and a central policy layer.
- Unguessable IDs are not a control on their own.
RED FLAGS
- 'The UI only shows the user their own invoices.'
- Switching to UUIDs as the whole fix.
- Checking roles but not ownership of the specific record.
LIKELY FOLLOW-UPS
- How would you test for this across a large API?
- What is function-level authorization (BFLA)?
How did your answer compare?
SOURCES
How should you store user passwords?
Reveal model answer
Never store them reversibly; store a slow, salted, memory-hard hash produced by a dedicated password hashing function. OWASP's current recommendation is Argon2id with at least 19 MiB of memory, 2 iterations, and parallelism 1; scrypt if Argon2id is unavailable; bcrypt with a work factor of at least 10 only for legacy systems, noting its 72-byte input limit; and PBKDF2-HMAC-SHA256 with 600,000 iterations where FIPS compliance requires it. The algorithm generates a unique salt per password and the library stores it in the encoded hash, so identical passwords produce different hashes. Optionally add a pepper kept in a secrets manager, separate from the database, and rehash on login when you raise parameters.
WHAT A STRONG ANSWER COVERS
- Hashing, not encryption, with a purpose-built slow function.
- Argon2id as the default, with rough parameters.
- Per-password salts handled by the library.
- bcrypt's 72-byte limit and PBKDF2's FIPS role.
- Upgrading parameters by rehashing on next login.
RED FLAGS
- SHA-256 or MD5, salted or not.
- Encrypting passwords so support can recover them.
- A single global salt.
LIKELY FOLLOW-UPS
- How would you migrate a table of unsalted SHA-1 hashes without forcing resets?
- Why is a fast hash bad here when it is good elsewhere?
How did your answer compare?
SOURCES
Server-side sessions or JWTs for a web app's login? What are the trade-offs?
Reveal model answer
A server-side session stores state in a store and gives the browser an opaque random ID in a `__Host-` cookie with `Secure`, `HttpOnly`, and `SameSite`; it is easy to revoke, regenerate on login, and expire on idle and absolute timeouts, at the cost of a lookup per request. A JWT is a signed, self-contained claim set that services can verify without a lookup, which suits service-to-service and federated tokens, but it cannot be revoked before expiry without reintroducing state (short lifetimes, refresh tokens, or a denylist keyed on `jti`). JWT pitfalls include accepting `alg: none`, algorithm confusion when a public key is treated as an HMAC secret (pin the expected algorithm), and putting readable sensitive data in claims. For a first-party browser app, a session cookie is usually the simpler and safer default; storing JWTs in `localStorage` exposes them to any XSS.
WHAT A STRONG ANSWER COVERS
- Revocation and logout are the core trade-off.
- Session cookie attributes and regeneration on login to prevent fixation.
- JWT verification pitfalls: `alg: none`, algorithm confusion, pinning algorithms.
- Token storage location and its XSS exposure.
- Matches the choice to the use case rather than declaring a winner universally.
RED FLAGS
- 'JWTs are more secure because they are encrypted.' Most are only signed and readable.
- Logout that only deletes the cookie or token client side.
- Long-lived JWTs with no revocation story.
LIKELY FOLLOW-UPS
- How do refresh token rotation and reuse detection work?
- Where would you keep tokens in a SPA that talks to a separate API?
How did your answer compare?
SOURCES
A teammate says CORS protects the API from other sites. Is that right?
Reveal model answer
Not really. CORS is a browser mechanism that relaxes the same-origin policy, letting a server opt in to sharing responses with scripts on other origins; it is not access control. Simple requests (GET, HEAD, or POST with form or text content types and safelisted headers) are sent without a preflight, so the server still receives and executes them, and only the response is hidden from the calling script; curl and server-to-server clients ignore CORS entirely. The dangerous configurations are reflecting any `Origin` back with `Access-Control-Allow-Credentials: true`, or loose regexes that match attacker subdomains; note that the browser forbids `*` with credentialed requests. Protect the API with authentication, authorization, and CSRF defenses, and treat CORS as a deliberate allowlist of origins that may read responses.
WHAT A STRONG ANSWER COVERS
- CORS relaxes the same-origin policy; it does not add protection.
- Simple requests reach the server without preflight.
- Non-browser clients ignore CORS.
- Reflected origins with credentials are the classic misconfiguration.
- `*` cannot be combined with credentials.
RED FLAGS
- Using CORS as the CSRF defense.
- Fixing a CORS error by reflecting every origin with credentials allowed.
- Thinking a preflight happens for every cross-origin request.
LIKELY FOLLOW-UPS
- Why does adding a custom header force a preflight, and how does that help CSRF defenses?
How did your answer compare?
SOURCES
What is a Content Security Policy, and how would you roll out a strict one?
Reveal model answer
CSP is a response header that tells the browser which sources of script, styles, frames, and other resources the page may use, mainly to limit the damage of XSS. A strict policy uses per-response nonces or hashes instead of host allowlists, for example `script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none'`, because host allowlists tend to include domains that serve attacker-usable scripts and grow unmaintainable. `'strict-dynamic'` lets nonced scripts load their dependencies, and browsers ignore `'unsafe-inline'` when a nonce or hash is present. Roll out with `Content-Security-Policy-Report-Only` and a `report-to` endpoint, fix violations (inline handlers, `javascript:` URLs, `eval`), then enforce; add `frame-ancestors 'none'` unless the page must be embedded, to stop clickjacking.
WHAT A STRONG ANSWER COVERS
- CSP is defense in depth for XSS, not a replacement for encoding.
- Nonce- or hash-based policies beat host allowlists.
- `'strict-dynamic'`, `object-src 'none'`, `base-uri 'none'`.
- Report-only first, then enforce.
- `frame-ancestors` for clickjacking; it replaces X-Frame-Options.
RED FLAGS
- A policy with `'unsafe-inline'` and `'unsafe-eval'` described as strict.
- A static nonce reused across responses or baked into cached HTML.
- Enforcing on day one without report-only and breaking production.
LIKELY FOLLOW-UPS
- How do nonces work with a CDN-cached or statically generated page?
- What are Trusted Types?
How did your answer compare?
SOURCES
How should an application and its CI pipeline handle secrets, and what do you do when one leaks?
Reveal model answer
Keep secrets out of code, images, and logs; store them in a central secrets manager with per-secret least-privilege access and audit logs, and inject them at runtime. Prefer short-lived, dynamically issued credentials, such as CI jobs exchanging an OIDC token for temporary cloud credentials instead of storing long-lived keys, and automate rotation. Catch leaks early with pre-commit and CI secret scanning. When a secret leaks, revoke it first, then rotate and redeploy, check access logs for use during the exposure window, and only then clean history; deleting the commit or restarting the app does not invalidate a credential someone already copied.
WHAT A STRONG ANSWER COVERS
- Central secrets manager, least privilege, audit trail.
- Short-lived credentials and OIDC federation for CI.
- Secret scanning before commit and in CI.
- Leak response: revoke, rotate, investigate usage, then clean up.
- Secrets in images or env dumps leak through layers and logs.
RED FLAGS
- 'We removed it in the next commit.'
- Committing an encrypted .env with the key next to it.
- Sharing one admin credential across every environment.
LIKELY FOLLOW-UPS
- How do you keep a secret out of a Docker image built in CI?
- How do fork pull requests interact with CI secrets?
How did your answer compare?
SOURCES
How would you reduce supply-chain risk in a JavaScript project?
Reveal model answer
Your dependencies, build tools, and CI run with your privileges, so a compromised package or pipeline is a compromise of you; OWASP made Software Supply Chain Failures its own 2025 category, citing incidents like SolarWinds and the 2025 Shai-Hulud npm worm, which used post-install scripts to steal tokens and republish itself into other packages. Controls: commit the lockfile and install with `npm ci` or `pnpm install --frozen-lockfile`, keep the dependency tree small, disable or allowlist install scripts, and update deliberately (with a short delay before adopting brand-new versions) rather than floating ranges. Monitor with an SBOM and vulnerability feeds (OSV, advisories), and harden the pipeline: MFA, least-privilege and short-lived publish tokens, pinned CI actions by commit SHA, and signed builds with provenance.
WHAT A STRONG ANSWER COVERS
- Covers both dependencies and the build or CI pipeline.
- Lockfiles and frozen installs for reproducibility.
- Install scripts as an execution vector.
- SBOM plus continuous vulnerability monitoring.
- Pipeline hardening: MFA, scoped tokens, pinned actions, provenance.
RED FLAGS
- 'We run npm audit' as the whole strategy.
- Auto-merging every dependency update the moment it is published.
- Ignoring transitive dependencies.
LIKELY FOLLOW-UPS
- What does an SBOM let you do during an incident like Log4Shell?
- Why pin GitHub Actions to a commit SHA rather than a tag?
How did your answer compare?
SOURCES
How would you run a lightweight threat model for a new feature?
Reveal model answer
Answer the four questions from the Threat Modeling Manifesto: what are we working on, what can go wrong, what are we going to do about it, and did we do a good enough job. Start with a data flow diagram showing processes, data stores, external entities, and trust boundaries, because boundaries are where attacks enter. Walk each element and flow with STRIDE (spoofing, tampering, repudiation, information disclosure, denial of service, elevation of privilege), record threats with mitigations, owners, and accepted risks, and turn them into tickets and tests. Do it during design, keep it short enough that teams actually do it, and revisit it when the design changes.
WHAT A STRONG ANSWER COVERS
- The four questions as the structure.
- Data flow diagram with explicit trust boundaries.
- STRIDE mapped to the properties it violates.
- Outputs are tracked mitigations and accepted risks, not a document that dies.
- Early in design and kept up to date.
RED FLAGS
- Treating threat modeling as a pre-launch penetration test.
- A list of generic threats with no link to the actual design.
LIKELY FOLLOW-UPS
- Threat model a password reset flow.
How did your answer compare?
SOURCES