Senior Interview Questions: API Security
What interviewers ask about API security, the answers that signal experience, and the ones that do not.
What is the most common API vulnerability?
Broken object level authorization — BOLA, or IDOR. The endpoint authenticates correctly and then returns object 1044 to the person who owns 1043. It is number one on the OWASP API Top 10, it is trivially exploitable, and no scanner finds it because only the application knows who owns what.
How do you prevent it?
Put the check inside the query, not after the fetch: findOne({ id, userId: req.user.id }), returning 404 rather than 403 so you do not confirm existence. Then push it below the application — a scoped repository, an ORM global scope, or row-level security in the database — so a query written in a hurry cannot cross a boundary.
Why 404 and not 403?
403 confirms the object exists. Over many requests that is an enumeration oracle: an attacker maps your id space without reading anything.
Can a WAF stop BOLA?
No, and this is the question that separates levels. The request is indistinguishable from a legitimate one — valid token, normal path, no payload. A WAF has no way to know that invoice belongs to someone else. It is a layer for commodity scanning and virtual patching, not an authorization control.
What is API6 — unrestricted access to sensitive business flows?
The category where nothing is broken. Every request is authenticated, authorized and valid; there are simply ten thousand of them from a script, buying all the stock or scraping the catalogue. The defence is economic — per-account limits, account age gates, cost asymmetry, randomised queues — not a code fix.
How do you validate input?
Allowlist, at the boundary, rejecting unknown fields rather than stripping them. Validate type, length, range, format, cardinality and depth. Then use the parsed result, not the raw body.
Where does encryption help, and where does it not?
TLS protects transit. Disk encryption protects a stolen drive. Neither protects against SQL injection or BOLA, because your application holds the key and the database hands back plaintext. Application-level encryption with KMS-held keys is what protects against a database compromise — at the cost of not being able to index or search the column.
Example
# The question you should be ready to answer in thirty seconds
"Here is an endpoint. What is wrong with it?"
app.get('/api/invoices/:id', auth, async (req, res) => {
res.json(await db.invoices.findById(req.params.id));
});
# Three findings, in severity order:
# 1. BOLA — the query is not scoped to the caller. Any authenticated user
# reads any invoice. Fix: findOne({ id, userId: req.user.id }), 404 on miss.
# 2. Excessive data exposure — the model is serialised whole, so every
# column now and in future is published. Fix: an explicit serializer.
# 3. No 404 handling — findById returning null produces a 200 with `null`.
#
# Naming all three, ranked, in thirty seconds, is a strong answer.When to use it
- A candidate names BOLA as the top API risk and immediately explains why scanners cannot find it.
- An interviewer shows a five-line handler and the candidate ranks three findings by severity rather than listing them.
- A discussion about WAFs is resolved by the candidate explaining what an edge layer structurally cannot see.
More examples
Rapid-fire, with the follow-ups
Reframing the Top 10 question rather than reciting the list is the highest-leverage single answer here — it signals judgement in the first thirty seconds.
Q: "Walk me through the OWASP API Top 10."
A: Do not recite ten items. Say: "Three of the ten are authorization —
object level, function level and property level — and that is where the
real breaches come from. Injection does not have its own slot any more,
which tells you how the risk profile shifted." Then name API6 as the one
people miss. That answer shows judgement; a list shows memorisation.
Q: "How do you stop mass assignment?"
A: An allowlist of writable fields, plus schema validation that REJECTS
unknown keys rather than stripping them.
FOLLOW-UP: "Why reject rather than strip?"
→ Stripping hides both attacks and client bugs. A 400 naming the
unexpected field surfaces a probe immediately and tells an honest
client exactly what it got wrong.
Q: "An endpoint takes a URL and fetches it. What do you worry about?"
A: SSRF. And the defence order matters: best is not accepting a URL at all;
then an allowlist of hosts; and only if it is genuinely arbitrary, resolve
the DNS yourself, validate EVERY returned address against private ranges,
and connect to the resolved IP — which is what closes DNS rebinding.
FOLLOW-UP: "Why connect to the IP rather than the hostname?"
→ Between your validation and your connection there is a second DNS
lookup. An attacker's server answers differently the second time.
Q: "How would you rate limit an API?"
A: Token bucket for general traffic, because bursts are normal. Layered keys —
per user, per tenant, per IP, plus a global circuit breaker. Cost-weighted,
so a report costs more than a health check. Atomic in Redis via Lua.
FOLLOW-UP: "What happens when Redis is down?"
→ Degrade to a conservative local limit. Never fail open — an attacker
who can degrade Redis has otherwise removed your rate limiting.
Q: "Is your internal API safe because it is on a private network?"
A: No. SSRF makes your own server issue the request, and then 'internal only'
means nothing. Add a misconfigured ingress, a compromised pod, or a VPN
with too many members. Network position is not a credential.
Q: "What logs would you want during an incident?"
A: Authentication events, every authorization DENIAL, privileged actions,
and reads of sensitive data — with a request id correlating across
services, and an append-only store the application cannot rewrite.
FOLLOW-UP: "Why denials specifically?"
→ A burst of ownership denials from one actor is the clearest enumeration
signal available, and it costs nothing to record.
Q: "Your dependency scanner is clean. Are you secure?"
A: It means no known CVE matched. It says nothing about authorization,
business logic, or a package that turned malicious yesterday and has no
advisory yet. That gap is closed by generated authorization tests, and by
blocking install scripts and delaying adoption of brand-new versions.The code-review exercise
Narrating a fixed set of questions rather than spotting bugs ad hoc is what makes the exercise repeatable — and interviewers can tell the difference.
// You will be shown something like this and asked what is wrong. Narrate a
// METHOD, not a list of guesses.
app.post('/api/reports', async (req, res) => {
const { userId, format, filters, webhookUrl } = req.body;
const data = await db.raw(`
SELECT * FROM transactions
WHERE user_id = ${userId}
AND created_at > '${filters.from}'
ORDER BY ${filters.sort}
`);
const report = await generateReport(data, format);
if (webhookUrl) await fetch(webhookUrl, { method: 'POST', body: report });
res.json({ report, rows: data });
});
// Narrate the seven questions:
//
// 1. AUTHENTICATION ❌ CRITICAL — no auth middleware at all. Anonymous.
// 2. OBJECT AUTHZ ❌ CRITICAL — userId comes from the BODY. Even with
// auth, any user reads anyone's transactions.
// 3. FUNCTION AUTHZ ❌ no role check on what looks like a reporting feature
// 4. INPUT VALIDATION ❌ CRITICAL — SQL injection in userId, filters.from,
// and filters.sort. The sort column is the nastiest:
// it cannot be parameterised, so it needs an allowlist.
// 5. OUTPUT SHAPING ❌ SELECT * plus `rows: data` returns every column.
// 6. RESOURCE BOUNDS ❌ no LIMIT, no timeout. One request can scan the table.
// 7. OUTBOUND ❌ CRITICAL — webhookUrl is client-supplied SSRF, and
// it POSTs the report CONTENTS to it. Exfiltration
// as a feature.
//
// Seven findings, four critical, in about ninety seconds — because you asked
// seven questions rather than hunting.
// Then offer the fix for the one they will ask about:
const schema = z.object({
format: z.enum(['csv', 'pdf']),
filters: z.object({
from: z.coerce.date(),
sort: z.enum(['createdAt', 'amount']), // ← allowlist: cannot be bound
}).strict(),
}).strict(); // note: no userId, no webhookUrl
app.post('/api/reports', auth, requireScope('reports:create'),
validate({ body: schema }), rateLimit({ cost: 25 }), async (req, res) => {
const rows = await db('transactions')
.where({ user_id: req.user.id }) // from the CREDENTIAL
.andWhere('created_at', '>', req.body.filters.from)
.orderBy(SORT_COLUMNS[req.body.filters.sort], 'desc')
.limit(10_000);
res.json({ report: await generateReport(rows, req.body.format) });
});
// Delivery goes to a REGISTERED webhook endpoint, validated at registration,
// never to a URL supplied in the request.The design question, and the trade-offs
Volunteering the failure analysis — what happens when each layer fails — is the move that most reliably distinguishes a senior answer from a comprehensive one.
"Design the security for a multi-tenant B2B API."
# Open with the question that shapes everything:
"How bad is a cross-tenant leak for this business?" For most B2B products the
answer is existential, which justifies defence in depth on isolation
specifically rather than spreading effort evenly.
# Then layer, and say what each layer catches when the one above fails:
1. IDENTITY
OIDC per tenant (enterprises bring their own IdP), tenant in the token,
never in the path. Memberships table: (user, tenant, role).
2. EDGE
Verify the token, route-level scopes, rate limit per tenant so one
customer cannot starve another, strip client-supplied internal headers.
3. APPLICATION
Tenant from the credential. A scoped repository that CANNOT be constructed
without a tenant. Ownership inside every query.
4. DATABASE
Row-level security on tenant_id, FORCED, with set_config(..., true) so
connection pooling cannot leak context between requests.
→ This is the layer that catches the query someone writes at 2am.
5. DETECTION
Audit every cross-tenant access attempt. Canary records per tenant.
Alert on any cross-tenant denial at all — there should be zero.
# Then volunteer the failure analysis, because they will ask:
"If someone forgets the tenant filter in a new query, RLS still blocks it and
the audit log records the attempt. If RLS were also misconfigured, the canary
fires. We would have a finding, not a breach."
# And the trade-offs you accepted, with numbers:
"RLS costs a few percent on query latency and makes some admin tooling need an
explicit bypass, which we made loud and audited. We chose shared-schema over
database-per-tenant because we have 4,000 tenants and per-database migrations
would dominate our operations. If we had 40 enterprise tenants I would
reconsider."
# Close with what you would NOT build:
"I would not build the identity provider. OIDC is a solved problem and running
one badly is worse than buying one. I would build the tenancy model, because
that is specific to us and nobody sells it."
# The pattern throughout: layer, state what each layer catches, name the
# trade-off with a number, and say what you would not build.
Discussion