API Security
API-layer risk — the OWASP API Top 10, excessive data exposure, rate limiting, GraphQL introspection/depth abuse, versioning of shadow endpoints, webhook signing and error-handling exposure.
12 questions
JuniorTheoryVery commonHow do API keys, OAuth2 access tokens and mutual TLS (mTLS) differ as API credentials?
How do API keys, OAuth2 access tokens and mutual TLS (mTLS) differ as API credentials?
A key is a long-lived shared string naming a caller; an OAuth2 access token is short-lived and scoped to one grant; mTLS proves the client by certificate. A key baked into a client build is extractable, so it identifies but proves nothing.
Common mistakes
- ✗Calling a key shipped to a client a secret rather than a public identifier
- ✗Passing keys in the URL, where proxies, browser history and logs retain them
- ✗Treating mTLS as encryption only and missing that it authenticates the client
Follow-up questions
- →How would you scope and rotate a partner key without breaking their integration?
- →What server-side control still protects you when a public key string leaks?
JuniorTheoryVery commonWhat is the OWASP API Security Top 10, and why is broken object-level authorization first?
What is the OWASP API Security Top 10, and why is broken object-level authorization first?
It ranks the risks specific to APIs, where authorization failures dominate. BOLA leads because an API hands out object ids directly, so every route must verify on the server that the caller owns the object it names.
Common mistakes
- ✗Reading it as a reordered web Top 10 instead of an API-specific risk ranking
- ✗Believing unguessable identifiers replace a server-side ownership check
- ✗Checking object authorization on writes only and leaving reads open
Follow-up questions
- →How does this differ from broken function-level authorization on an admin-only route?
- →Why do deprecated API versions often keep a risk alive after the current version is fixed?
JuniorTheoryCommonWhy do verbose API errors leak internals, and what response pattern replaces them?
Why do verbose API errors leak internals, and what response pattern replaces them?
Default handlers return the exception text, stack frames, SQL fragments or library versions, which map storage, paths and dependencies for the caller. Return a generic message plus a correlation id, and log the detail server-side.
Common mistakes
- ✗Assuming a stack trace is useless to a caller because it is unformatted text
- ✗Removing detail from the response without logging it anywhere for responders
- ✗Trusting the framework's production defaults instead of verifying the error shape
Follow-up questions
- →How does a correlation id let support diagnose without exposing internals?
- →Why should a denied object and a missing object return the same response?
MiddleDebuggingCommonAn access log shows v1 order reads across owners while v2 is protected — find and fix the flaw
An access log shows v1 order reads across owners while v2 is protected — find and fix the flaw
The ownership check exists only as a gateway filter on v2, so the deprecated v1 route reaches the same service and returns any order by id. Move the object check into the order service, where every version passes, and retire v1.
Open full question →Common mistakes
- ✗Treating a gateway filter as the authorization control for every route behind it
- ✗Forgetting that a deprecated version still reaches the current service
- ✗Fixing the identifier format or the status code instead of the missing check
Follow-up questions
- →How would you find every route still reachable but absent from the current specification?
- →What alert on this log would have surfaced the pattern on the first day?
MiddleTheoryCommonWhat is excessive data exposure when an API returns whole objects, and how is it fixed?
What is excessive data exposure when an API returns whole objects, and how is it fixed?
The handler serialises the whole record and leaves filtering to the client, so password hashes, internal flags or another user's contacts travel in the JSON. Shape the response server-side from an explicit output schema per role.
Common mistakes
- ✗Believing a field the UI never renders is not disclosed to the caller
- ✗Delegating field selection to the client instead of the server
- ✗Confusing response size or pagination with least-exposure filtering
Follow-up questions
- →How would you keep a new database column out of the response by default?
- →What test would catch a serializer change that starts emitting an internal flag?
MiddleTheoryCommonHow can an extra field in an API request body write a privileged property, and what stops it?
How can an extra field in an API request body write a privileged property, and what stops it?
Binding the request body straight onto a model lets an unexpected property such as role or isVerified reach a column the caller may not set. Bind only an allowlist of writable fields per route and reject other bodies.
Common mistakes
- ✗Assuming object-level ownership authorises every property of that object
- ✗Using a denylist of sensitive fields, which new columns silently escape
- ✗Trusting the published specification to constrain what callers actually send
Follow-up questions
- →Why is rejecting unknown properties safer than silently dropping them?
- →How do you keep the allowlist correct when the entity gains columns each sprint?
MiddleTheoryCommonWhy is per-IP rate limiting alone weak, and what else bounds API resource consumption?
Why is per-IP rate limiting alone weak, and what else bounds API resource consumption?
One address can front a whole office behind NAT while an abuser rotates addresses cheaply, so per-IP limits punish shared users and miss the attacker. Limit per identity or key first, and cap page size, body size and concurrency.
Common mistakes
- ✗Treating the client IP as a stable identity despite NAT and rotation
- ✗Counting requests only, while one request can still pull a million rows
- ✗Applying a single global cap that one noisy account exhausts for everyone
Follow-up questions
- →How would you limit an unauthenticated route without blocking a shared office?
- →What response and headers tell a well-behaved client to back off?
MiddleTheoryCommonHow do you verify an inbound webhook so a forged or replayed delivery is rejected?
How do you verify an inbound webhook so a forged or replayed delivery is rejected?
Recompute an HMAC over the exact raw body with the shared secret and compare it to the signature header with a constant-time function. Sign a timestamp into the payload, refuse anything outside a short window, and record delivery ids.
Common mistakes
- ✗Hashing a re-serialised body instead of the exact bytes that were signed
- ✗Comparing signatures with a short-circuiting equality check
- ✗Verifying the signature only after the handler has already applied the change
Follow-up questions
- →Why must the raw body be captured before any JSON parsing middleware runs?
- →How would you rotate a webhook secret without dropping in-flight deliveries?
MiddleTheoryOccasionalHow do you harden a production GraphQL endpoint against introspection, deep queries and batching?
How do you harden a production GraphQL endpoint against introspection, deep queries and batching?
Disable introspection and the playground outside development, reject queries above a depth and complexity budget before execution, and cap batch size and page arguments. Authorization belongs in each resolver, not only at the root.
Common mistakes
- ✗Assuming one endpoint means one authorization decision for the whole query
- ✗Leaving introspection and the playground enabled in production
- ✗Measuring cost after execution, once the expensive query has already run
Follow-up questions
- →Why does disabling introspection not hide the schema from a determined caller?
- →How do persisted queries change what you must limit at the endpoint?
MiddleDesignOccasionalYour platform exposes a public write API where POST /v1/orders and PATCH /v1/users bind the
incoming JSON body straight onto ORM entities shared by three services. An incident review found
that a customer had set an undocumented discountPercent property on an order, and that a support
agent had raised their own account tier the same way.
Constraints:
- the entities gain new columns most sprints, and nobody updates the API documentation
- existing integrators must keep working, so current bodies cannot start failing without notice
- there is no gateway able to inspect request bodies
- two of the three services are maintained by another team
Design the write contract that makes this class of defect impossible. Cover how a request body is
defined, what happens to a property nobody declared, how a column added next sprint behaves by
default, and what evidence would show the rule still holds for a route added next quarter.
Your platform exposes a public write API where POST /v1/orders and PATCH /v1/users bind the incoming JSON body straight onto ORM entities shared by three services. An incident review found that a customer had set an undocumented discountPercent property on an order, and that a support agent had raised their own account tier the same way. Constraints: - the entities gain new columns most sprints, and nobody updates the API documentation - existing integrators must keep working, so current bodies cannot start failing without notice - there is no gateway able to inspect request bodies - two of the three services are maintained by another team Design the write contract that makes this class of defect impossible. Cover how a request body is defined, what happens to a property nobody declared, how a column added next sprint behaves by default, and what evidence would show the rule still holds for a route added next quarter.
Give each route its own input schema naming only the fields it accepts, and build the entity from that schema instead of the raw body, so a new column stays unwritable. Reject undeclared properties and assert the rule in CI.
Common mistakes
- ✗Choosing a denylist, which every newly added column silently escapes
- ✗Relying on documentation or review rather than an enforced schema
- ✗Placing the only check in one service that the other two do not share
Follow-up questions
- →How would you roll out rejection of unknown fields without breaking live integrators?
- →What would you log while the notice period runs to size the impact?
SeniorDesignOccasionalA GraphQL gateway fronts four internal services for both a first-party web client and paying
partners. One partner query nested customers into orders into line items into products and held
a database node for eleven minutes; a second query returned supplier cost prices that no partner
is allowed to see, reached through an order the partner did own.
Constraints:
- partners send arbitrary queries, so persisted queries alone are not an option this quarter
- the schema is federated, and each service owns its own resolvers
- the web client legitimately sends deep queries on a few screens
- authorization data lives in the token claims plus a per-service ownership table
Design the protection. Cover how the cost of a query is bounded before it executes, where the
authorization decision for a nested field is made, how the two clients get different budgets, and
what you would monitor to see abuse that stays under every limit.
A GraphQL gateway fronts four internal services for both a first-party web client and paying partners. One partner query nested customers into orders into line items into products and held a database node for eleven minutes; a second query returned supplier cost prices that no partner is allowed to see, reached through an order the partner did own. Constraints: - partners send arbitrary queries, so persisted queries alone are not an option this quarter - the schema is federated, and each service owns its own resolvers - the web client legitimately sends deep queries on a few screens - authorization data lives in the token claims plus a per-service ownership table Design the protection. Cover how the cost of a query is bounded before it executes, where the authorization decision for a nested field is made, how the two clients get different budgets, and what you would monitor to see abuse that stays under every limit.
Score every query against depth and field-cost weights before execution and reject it over a budget set by the caller's client class. Each service authorises its own resolvers, since a permitted parent must not open a forbidden child.
Common mistakes
- ✗Authorising the root field and letting nested resolvers inherit that decision
- ✗Bounding cost after execution by timeout or payload size instead of before it
- ✗Giving every client one budget, so the strictest partner limit throttles your own client
Follow-up questions
- →How would you weight fields so the cost score tracks real database work?
- →What signal separates a heavy but legitimate partner from a slow enumeration?
SeniorDesignOccasionalA multi-tenant SaaS API serves customer admins, their end users, and an internal support console
from the same handlers. Each response is produced by serialising the domain object, and the web
client hides what a role should not see. Two audit findings landed the same week: an end user
read a colleague's phone number from a JSON response, and a tenant saw another tenant's account
identifier inside an error payload from a shared reporting endpoint.
Constraints:
- the same endpoints must keep serving all three audiences
- domain objects gain fields regularly and are shared with an events pipeline
- support genuinely needs broader visibility than any customer
- no response may reveal that an object in another tenant exists
Design the response contract. Cover what decides which fields a caller receives, how a field added
next month behaves, how the tenant boundary is enforced, and how you would detect a regression.
A multi-tenant SaaS API serves customer admins, their end users, and an internal support console from the same handlers. Each response is produced by serialising the domain object, and the web client hides what a role should not see. Two audit findings landed the same week: an end user read a colleague's phone number from a JSON response, and a tenant saw another tenant's account identifier inside an error payload from a shared reporting endpoint. Constraints: - the same endpoints must keep serving all three audiences - domain objects gain fields regularly and are shared with an events pipeline - support genuinely needs broader visibility than any customer - no response may reveal that an object in another tenant exists Design the response contract. Cover what decides which fields a caller receives, how a field added next month behaves, how the tenant boundary is enforced, and how you would detect a regression.
Serialise through an explicit output view chosen by the caller's role, never the domain object, so a field added later stays invisible until a view lists it. Scope every query by tenant in the data layer and keep errors generic.
Common mistakes
- ✗Letting a new domain field reach responses by default instead of staying hidden
- ✗Enforcing the tenant boundary above the data layer, where one query can miss it
- ✗Distinguishing a foreign object from a missing one in the error response
Follow-up questions
- →How would you give support broader visibility without widening the customer views?
- →What would a golden-response test assert so a new field fails the build?