Scenario Debugging
Security and Auth Scenario Bank
Fourteen “you found a flaw” cases: stored XSS, IDOR, password-reset abuse, open redirects, SSRF, credential stuffing, a leaked key and a hacked third-party script.
On this page
In one line: answer a security scenario in four parts: Impact (what can an attacker do?), Reproduce and confirm, Fix at the root cause (not only the symptom), and Prevent the whole class of bug (tests, safe defaults, scanning). If the problem is live, contain it first (stop the damage, for example by turning off the feature).
This lesson has fourteen realistic scenarios. They build on Browser Contract Lessons 02, 03, 04 and 07 and Backend Lesson 06.
The scenarios
1. "A user's profile bio executes JavaScript for anyone who views it."
Impact: this is stored XSS (cross-site scripting). XSS means an attacker makes a page run the attacker's JavaScript. "Stored" means the code is saved in your data and runs in other people's browsers. The attacker can steal sessions (the proof that a user is logged in) or act as other users.
Reproduce: put <img src=x onerror=…> in the bio.
Fix at the root:
- Find where raw HTML is rendered (
dangerouslySetInnerHTML,innerHTML, markdown renderers). React escapes normal text by default, so the hole is almost always one of these places. - Show text as text. Escaping means turning characters such as
<into harmless text (<) so the browser does not treat them as HTML. If you must allow HTML, clean it at render time with an allow-list sanitiser (a tool that keeps only tags on a safe list and removes everything else). - Check URLs that users give you (allow only
https:,http:ormailto:; ajavascript:link runs code when clicked, although React 19 blocks it inhref). - Add a CSP (Content Security Policy, a rule you send in an HTTP header that tells the browser which scripts may run), so injected inline script cannot run.
- Check every place that shows the same data: the profile page, admin tools, emails and exports.
Prevent: add lint rules for dangerouslySetInnerHTML and innerHTML. Write security tests with XSS payloads. Start the CSP in report-only mode, then enforce it. Use HttpOnly cookies (cookies that JavaScript cannot read) to limit the damage. They stop cookie theft, but injected code can still act as the user. (Browser Contract, Lesson 07)
2. "Changing /orders/123 to /orders/124 shows someone else's order."
Impact: this is broken object-level authorization, also called IDOR (insecure direct object reference). Authorization means checking what a logged-in user is allowed to do. Authentication only checks who the user is. The server does not check who owns the object. Many people's data can be exposed.
Fix: limit every query to the owner or tenant (where: { id, userId }). Return 404 (not found) for objects that belong to others, so the attacker cannot tell if the object exists. Keep the access rules in one place. Check all endpoints that accept an ID, including nested resources, exports and file downloads.
Prevent: write cross-user tests for every endpoint. Use a deny-by-default policy layer (access is refused unless a rule allows it). UUIDs (long random IDs) help a little, because they are hard to guess, but they are not the fix. (Backend, Lesson 06)
3. "Password reset can be abused to take over accounts."
Common flaws and fixes:
- Tokens that can be guessed or reused → use long random tokens that work only once, store only a hash of them in the database (a one-way scramble, so a stolen database does not give working tokens), and give them a short life (for example 15 to 60 minutes).
- Token in the URL leaks through the
Refererheader (it tells the next site which page the user came from) or logs → setReferrer-Policy: no-referrer, do not log URLs that contain tokens, and make the token invalid once it is used. - Host header injection (the
Hostheader says which site the request is for; the attacker sets it to their own domain, and the reset link is built from it) → build links from a base URL that you set in config. - Account enumeration (the reply says "no such email", so attackers learn which emails exist) → give the same reply for all emails.
- No rate limit (a cap on how many requests a client may send in a time window) → limit by account and by IP address.
- Sessions stay valid after a reset → end the other sessions.
- Email change without asking for the password again → ask for the current password, and send a notice to the old address.
Prevent: design the flow with a threat model (a list of how it could be attacked). Add security tests. Ask for MFA (multi-factor authentication: a second proof of identity, such as a code from a phone) for sensitive actions.
4. "A session token was stolen. What limits the damage, and what do you do now?"
Contain: revoke (cancel) the affected sessions or tokens. If the signing key leaked, rotate it too (replace it with a new one). Force the account to log in again. Block the source if it is abusing the system.
Investigate: how was it stolen? It could be XSS, malware, leaked logs, a proxy or a shared device. Check the audit logs (records of who did what) for what the attacker did.
Reduce future damage:
- short-lived access tokens (a token that is valid for a few minutes, so a stolen one stops working soon)
- refresh-token rotation with reuse detection. A refresh token is a long-lived token used only to get a new access token. With rotation, each one works once, and a second use is a warning sign
- tying sessions to extra signals, such as the device, or asking again for risky actions (step-up auth)
- cookie flags:
HttpOnly(JavaScript cannot read it),Secure(sent only over HTTPS) andSameSite(not sent on most cross-site requests) - notices for logins from a new device
(Browser Contract, Lessons 02 and 03)
5. "A pentest says our CORS configuration reflects any origin with credentials."
Impact: a pentest (penetration test) is a test where security experts try to break your system. CORS (Cross-Origin Resource Sharing) is a set of browser rules that say which other websites may call your API from the browser and read the answer. An origin is the scheme, host and port of a site, for example https://app.example.com. If you accept every origin with credentials, any website can make logged-in requests as the user and read the answers.
Fix: use an allow-list of exact origins. Never copy the Origin header back without checking. Never use * with credentials. Do not allow the null origin, and do not check origins with endsWith or loose patterns (evil-example.com would pass). Add Vary: Origin (so caches keep a separate copy for each origin). Keep CORS before auth, so preflight requests work. A preflight is an OPTIONS request that the browser sends first to ask permission. It carries no credentials. Remember that CORS is not the security of the API. The API must still check permission for every request.
Prevent: use a separate config for each environment. Add a test that an untrusted origin gets no CORS headers. (Browser Contract, Lesson 04)
6. "After login, ?next=https://evil.com redirects users to an attacker's site."
Impact: this is an open redirect: your site sends users to any address that a URL parameter names. It allows phishing (tricking people into giving up secrets) from your trusted domain, and it can leak OAuth codes (the one-time codes in the login flow).
Fix: accept only relative paths or hosts from an allow-list. Parse the value with new URL(value, base) and compare the origin with your own. Reject anything that starts with // or /\, or that contains a scheme (such as https:). Browsers treat \ like /, and they ignore tab characters inside a URL, so a simple startsWith("/") check is not enough.
Prevent: use one shared redirect helper. Write tests with tricky inputs (//evil.com, /\evil.com, /<tab>/evil.com, https://evil.com, javascript:alert(1)).
7. "A security scan reports SQL injection in the search endpoint."
Impact: in SQL injection, the attacker's input becomes part of your SQL command (SQL is the language used to talk to a database). They can steal data, change it, or do worse.
Reproduce (on a test copy only): send a single quote ' in the search box. A database error or a changed result is a strong sign.
Fix: use parameterised queries everywhere. The query text and the values travel separately, using placeholders such as $1 (an ORM, which is a library that builds queries for you, does this too), so input is always treated as data. For names that cannot be parameters (sort columns, table names), use an allow-list. Never build SQL by joining strings, also not in Prisma's $queryRawUnsafe or similar functions. (Prisma's $queryRaw tagged template is safe, because it sends the values as parameters.) Give database users least privilege (only the rights they need).
Prevent: add lint rules against raw SQL strings. Review raw queries in code review. Use a WAF (web application firewall, a filter in front of your app that blocks known attack patterns) as a second layer. Write tests with attack payloads. (Backend, Lessons 03 and 08)
8. "The 'link preview' feature can be used to read our cloud metadata service."
Impact: this is SSRF (server-side request forgery). The server fetches URLs that the attacker chooses. So the attacker can reach internal services and cloud credentials. The cloud metadata service is a special internal address (169.254.169.254 on AWS) that gives a cloud server its settings and temporary credentials.
Fix:
- Allow only
httpandhttps. - Look up the hostname and block private, loopback and link-local addresses (internal ranges: private ones such as
10.x.x.x, loopback127.0.0.1which means the server itself, and link-local169.254.x.x). Do the same checks for IPv6 (::1,fc00::/7,fe80::/10) and for odd spellings of an IP address. Other clouds have similar metadata addresses. Connect to the address you checked, and check again after every redirect. DNS (the system that turns names into addresses) can give a different answer the second time, which is called DNS rebinding. - Limit redirects, response size and time.
- Run the fetcher in an isolated network with no access to internal services.
- On AWS, require IMDSv2 (the safer version of the metadata service, which needs a session token) on cloud instances.
Prevent: use one separate, locked-down fetch service. Use allow-lists where you can.
9. "A critical vulnerability is announced in a library we use. What do you do?"
Respond:
- Find out the exposure. Is the weak version in production, and can an attacker reach it? Use the lockfile (the file that records exact dependency versions) and an SBOM (software bill of materials, a list of all software parts).
- Patch or reduce the risk (upgrade, config flag, or a WAF rule that blocks the attack until you can upgrade), in order of priority.
- Deploy and check.
- Look in the logs for signs of attack.
- Tell the people who need to know.
Prevent: scan dependencies automatically and open update PRs (pull requests, which are proposed code changes). Keep a short path to patch. Use few dependencies. Keep a list of what runs where.
10. "Credential stuffing: thousands of failed logins from many IPs."
Credential stuffing means attackers try email and password pairs leaked from other sites, hoping people reused them. It differs from brute force, where the attacker guesses passwords.
Detect: watch the rate of failed logins per account and per IP. Look for many accounts with the same password.
Respond: rate-limit by account and by IP. Add growing delays. Be careful with a hard account lockout: an attacker can use it to lock real users out. Slowing down is safer. Show a CAPTCHA (a test that is easy for people and hard for bots) or proof-of-work (a small calculation that the browser must do) after failures. Ask for MFA or step-up for risky logins. Block known bad networks. Tell affected users. Force a password reset for hacked accounts. Check passwords against breach lists (lists of passwords known from leaks).
Prevent: use MFA and passkeys (a login method based on a key pair stored on the user's device, with nothing to steal from your server). Use bot management at the edge (tools at the CDN that detect bots). Give generic error messages. Set alerts for unusual logins. (Practical Design Problems, Lesson 02)
11. "An API key was committed to a public Git repository."
Treat the key as stolen right away, even if you remove the commit. History, forks and scrapers keep a copy.
Do this: rotate or revoke the key first. Check usage logs for abuse. Remove it from history only as a second step. Move to secrets management: keep secrets in a secrets manager (a service that stores them safely and hands them to your app) and use short-lived credentials.
Prevent: use secret scanning (a tool that looks for keys in code; run it before commit and in CI, with push protection that refuses a push that contains a secret). Put env files (.env, which hold settings and secrets) in .gitignore. Use keys with least privilege. Use separate keys for each environment.
12. "Users can upload files, and one of them runs script when viewed."
Impact: an uploaded HTML or SVG file (SVG is an image format that can contain script), served from your own origin, can cause stored XSS or spread malware.
Fix: serve uploads from a separate domain that has no cookies, such as example-usercontent.com, not a subdomain of your main site, because a subdomain can share cookies and other trust with it. Also:
- send
X-Content-Type-Options: nosniff(the browser must trust the declared type and not guess) - send the correct
Content-Type - use
Content-Disposition: attachment(the browser downloads the file instead of showing it) for risky types - re-encode images (decode and save them again, which removes hidden code)
- check the magic bytes (the first bytes that show the real file type)
- scan for malware
- block file types that can run code
Prevent: upload straight to object storage (a service for files, such as S3), using short-lived upload links that the server creates. (Practical Design Problems, Lesson 04)
13. "A state-changing endpoint can be triggered from another site."
Impact: this is CSRF (cross-site request forgery). Another site makes the victim's browser send a request, and the browser adds the victim's cookies. The action is done as the victim.
Fix: use SameSite=Lax or Strict cookies. (A "site" is the registrable domain, so a.example.com and b.example.com count as the same site. A hacked subdomain can still attack, which is why you add tokens.) With Lax, the browser sends the cookie on cross-site requests only for top-level GET navigations. Use CSRF tokens (a secret value that the page must send back, and that another site cannot know), or check the Origin or Sec-Fetch-Site headers, on unsafe methods (POST, PUT, PATCH, DELETE). Never change state on GET. For JSON APIs, require a custom header or Content-Type: application/json, because another site cannot send these without a CORS preflight.
Prevent: turn on the framework's CSRF protection by default. Write tests that a cross-origin form post is rejected. (Browser Contract, Lesson 03)
14. "A third-party script we load from a CDN was compromised."
Impact: the attacker can run any code in every visitor's browser. They can steal credentials or skim payment data.
Contain: remove or block the script now (with a feature flag, a switch that turns a feature on or off without a deploy, or with a deploy). Purge the caches (delete the bad copy from the CDN and other caches). Check what data it could reach. Tell the right people if the rules require it.
Prevent: host critical scripts yourself. Use SRI (Subresource Integrity, a hash in the tag that lets the browser check the file) with pinned versions (an exact version that never changes by itself). Use a strict CSP, including connect-src to limit where data can be sent. Use as little third-party code as you can. Watch for changes in scripts. (Browser Contract, Lesson 07)
A baseline you should be able to recite
An interviewer may ask: "What security practices would you insist on from day one?" A good answer: HTTPS everywhere with HSTS (an HTTP header that tells browsers to use only HTTPS for your site), login with hashed passwords and MFA, permission checks on every object, input validation and parameterised queries, output encoding with a CSP, secure cookie flags, rate limiting, dependency and secret scanning, least privilege, logging and alerts that do not contain secrets, backups, and an incident-response plan.
Interview questions
Scenario: changing an ID in the URL shows another user's data. What do you do?
First I contain the problem by fixing the worst endpoints quickly. Then I fix the root cause. I limit every query to the owner or tenant, return 404 for other people's objects, and keep the access rules in one place. I check all endpoints that accept an ID, including nested resources and downloads. I add cross-user regression tests and look in the logs for earlier abuse.
Scenario: an API key is found in a public repository. What are the steps?
I rotate and revoke the key at once, because history and forks keep a copy. I check the usage logs for abuse. I replace the key with managed secrets and give keys least privilege. I add secret scanning with push protection in CI. Removing the commit alone is not enough.
Scenario: a feature fetches user-supplied URLs. What are the risks and defences?
The risk is SSRF. Attackers can reach internal services and cloud metadata. I allow only http and https. I look up the hostname and block private and link-local ranges, also after every redirect. I limit size, time and redirects. I run the fetcher in a restricted network and turn on the metadata-service protections.
Scenario: you suspect credential stuffing. How do you confirm and respond?
I look for many failed logins across many accounts and IPs. I rate-limit by account and IP, add delays and a CAPTCHA, and ask for MFA or step-up. I block abusive networks, force resets for hacked accounts, and check passwords against breach lists. I keep error messages generic.
Scenario: the CORS config reflects any origin with credentials. What is the risk and fix?
Any site could make logged-in requests and read the answers. I replace the copying of the origin with an exact allow-list. I add Vary: Origin. I never combine * with credentials. The API must still check permission, whatever CORS says.
Scenario: a library has a critical CVE. What is your process?
A CVE (Common Vulnerabilities and Exposures) is a public ID for a known security flaw. First I find out if the weak code is in production and if it can be reached (lockfile, SBOM). I patch or reduce the risk in order of priority. I deploy and check. I look for signs of attack in the logs and tell the right people. Then I improve the automation, so patching is faster next time.
Key takeaways
- Answer in this order: Impact, Reproduce, Fix the root cause, Prevent the class. Contain first when the problem is live.
- The same root causes appear again and again: missing permission checks, output that is not escaped, SQL built from strings, trusting input, leaked secrets, CORS and redirects that are too open, and third-party code.
- Defence in depth (CSP, cookie flags, rate limits, least privilege, scanning) means several layers protect you, like a door with a lock, an alarm and a camera. Then one mistake does not become a disaster.
What's next
Lesson 14: Architecture, Data and Team Judgement Scenarios. The last set: decisions about systems, migrations and people. Senior interviews give these as much weight as code.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Securing LLM Applications: The OWASP Top 10 for LLM Apps in Code · AI Engineering
- API Authentication and Authorization: API Keys, OAuth 2.0 and Scopes · API Design
- Authentication and Authorization on the Server · Backend in Depth
Related series