Most API breaches are not caused by clever exploits but by design decisions made before the first line of code: endpoints that trust a client-supplied ID, models that bind every field, and old versions nobody turned off. It covers the API contract, identifiers, authorisation placement, data exposure, error models, idempotency, versioning and deprecation, and how to make the secure path the default one for every team that builds an endpoint. It is written for API designers, architects, tech leads and the AppSec engineers who review their specifications. For the programme view (inventory, gateways, testing and monitoring across the estate) see the API Security Guide; this page goes one level deeper into the design choices themselves.
Key takeaways
- Design the contract first and make it enforceable. An OpenAPI (or protobuf, or GraphQL SDL) contract that declares authentication, types, lengths and allowed fields is the one artefact that design review, gateways, code generation and tests can all check against.
- Authorise every object and every field, on the server. OWASP API1, API3 and API5 are all authorisation failures; the design must say who may read or change each resource, property and function, and the check must live in a trusted service layer (ASVS 5.0 V8.3.1).
- Bind only what you mean to accept, return only what the caller may see. Separate request and response models per operation stop mass assignment (ASVS V15.3.3) and excessive data exposure at the source rather than relying on clients to ignore fields.
- Make failure boring and predictable. A single error format (RFC 9457 problem details), correct status codes, idempotency keys on unsafe retries and explicit limits turn abuse and edge cases into documented behaviour instead of leaks.
- Plan the end of life on day one. Versioning, Deprecation and Sunset headers and an inventory entry per version prevent the forgotten v1 endpoint that OWASP API9 describes.
Secure by default: what it means for an API
An API is secure by default when a developer who adds a new endpoint and does nothing special gets authentication, authorisation, validation, limits and safe errors anyway, and has to take a deliberate, reviewed step to weaken any of them.
| Design decision | Insecure default | Secure default | Risk it prevents (OWASP API Top 10 2023) |
|---|---|---|---|
| Authentication | Endpoints public unless someone adds a check | Global security requirement in the contract and middleware; public endpoints are an explicit, reviewed exception | API2 Broken Authentication |
| Object access | Fetch by ID from the path, return the record | Every repository call scoped by the caller's tenant and permissions | API1 Broken Object Level Authorization |
| Request binding | Deserialise the body straight into the database entity | Per-operation input model with an allow list of writable fields | API3 Broken Object Property Level Authorization |
| Response shape | Serialise the full entity and let the client filter | Per-operation output model, sensitive fields opt-in by role | API3 Broken Object Property Level Authorization |
| Function access | Admin routes hidden behind an unguessable path | Deny by default per route and method, role or scope required | API5 Broken Function Level Authorization |
| Limits | Unlimited page size, body size and query depth | Maximums declared in the contract and enforced at the edge and in code | API4 Unrestricted Resource Consumption |
| Business flows | Any authenticated caller can run a flow at machine speed | Flow-specific limits and abuse signals on purchase, sign-up, voucher and reset flows | API6 Unrestricted Access to Sensitive Business Flows |
| Outbound calls | Fetch whatever URL the client supplies | Allow-listed destinations, no client-controlled URLs where avoidable | API7 Server Side Request Forgery |
| Errors and headers | Framework default error pages and stack traces | Problem details with generic messages, hardened response headers | API8 Security Misconfiguration |
| Versions | Old versions left running indefinitely | Every version in the inventory with an owner and a sunset date | API9 Improper Inventory Management |
| Third-party APIs | Trust responses from partners and redirects | Validate upstream data like user input, TLS everywhere, no blind redirects | API10 Unsafe Consumption of APIs |
Each risk has its own deep guide: API1 Broken Object Level Authorization, API2 Broken Authentication, API3 Broken Object Property Level Authorization, API4 Unrestricted Resource Consumption, API5 Broken Function Level Authorization, API6 Unrestricted Access to Sensitive Business Flows, the SSRF Prevention Guide for API7, API8 Security Misconfiguration, API9 Improper Inventory Management and API10 Unsafe Consumption of APIs.
Contract-first design and what the specification must declare
Writing the contract before the code gives security review something concrete to read. A specification that only lists paths is documentation; one that declares constraints is a control. For an OpenAPI document, require the following before an API is approved for build.
- Global security requirement: declare the security scheme (OAuth 2.0, mutual TLS, API key) at the top level so it applies to every operation. In OpenAPI, an operation-level
securityblock overrides the global one and an empty array removes security entirely, so lint for empty arrays and require a written justification for each public operation. - Scopes per operation: list the OAuth scopes each operation needs (
orders:read,orders:write), not a single catch-all scope. Scopes are coarse function-level controls; object-level checks still happen in code. - Strict schemas: types,
format,maxLength,minimum/maximum,enumandpatternfor every field,maxItemsfor arrays, andadditionalProperties: falseon request bodies so unknown fields are rejected rather than silently bound. This is the positive, allow-list validation ASVS 5.0 V2.2.1 asks for, and V2.2.2 requires it to be enforced at a trusted service layer, not only in the client. - Separate read and write models: distinct schemas for create, update and response, with
readOnlyfields (IDs, owner, status, price) absent from input models. - Content types: the request media types the operation accepts. Anything else is rejected with 415; if the client's
Acceptcannot be satisfied, return 406 rather than echoing the requested type intoContent-Type(OWASP REST Security Cheat Sheet). - Limits: maximum page size, maximum body size, and for GraphQL the depth and cost limits (ASVS V4.3.1). See the GraphQL Security Guide.
- Error responses: the problem details schema for 400, 401, 403, 404, 409, 413, 415, 422 and 429, so clients and tests know what failure looks like.
Lint the contract in the pipeline (rules for missing security, missing maxLength, permissive additionalProperties, secrets in query parameters) and fail the build on violations. The same contract then drives gateway validation, server stubs and contract tests. The API Gateway Security Guide covers the gateway side.
Identifiers, resources and authorisation placement
Broken object level authorisation is the first risk in the OWASP list because so many APIs take an identifier from the path and fetch the record without asking whether this caller may see it. Design can make that mistake hard to write.
Identifier design
- Use non-sequential identifiers for externally visible resources: random UUIDs or opaque tokens make enumeration harder. This is defence in depth only; an unguessable ID is not an authorisation check.
- Prefer the caller's context to client-supplied owner IDs: design
GET /me/ordersor derive the tenant from the token rather than accepting?customerId=. - Never put secrets or personal data in URLs: tokens, API keys and identifiers such as email addresses end up in access logs, proxies and browser history. Send them in headers or the body (OWASP REST Security Cheat Sheet).
Where the check lives
ASVS 5.0 separates the three authorisation levels the design must cover: function level (V8.2.1), data or object level (V8.2.2, the IDOR and BOLA requirement) and field level (V8.2.3, BOPLA). V8.3.1 requires enforcement at a trusted service layer, and V8.3.3 requires that access to an object is based on the originating subject's permissions, not those of an intermediary service acting on its behalf. That last point matters in microservice designs: if the order service calls the billing service with its own powerful service identity and no end-user context, billing cannot enforce per-user rules. Propagate the user's identity, for example with token exchange, so downstream services can decide. See the OAuth Token Exchange and Delegation Guide.
| Pattern | How it works | Strength | Watch for |
|---|---|---|---|
| Scoped repository | Data access layer requires a principal and adds tenant and owner filters to every query | Hard to forget; one place to test | Raw queries and admin tools that bypass the layer |
| Policy engine (central decision point) | Handlers ask a policy service or library, passing subject, action and resource | Consistent rules, auditable decisions, good for complex sharing models | Latency, and handlers that skip the call |
| Gateway scope checks | Gateway rejects tokens lacking the operation's scope | Cheap function-level filtering at the edge | Cannot see object ownership; never sufficient alone |
| Row-level security in the database | Database enforces tenant predicates from session context | Strong backstop for multi-tenant data | Connection pooling that leaks session context; service accounts that bypass it |
For most teams the scoped repository plus gateway scope checks is the right baseline; add a policy engine when sharing rules (delegation, groups, per-record ACLs) outgrow simple ownership. ASVS V3.5.3 requires unsafe methods (POST, PUT, PATCH, DELETE) for state changes, and V4.1.4 requires unsupported methods to be blocked with 405.
Where identity fits: who is calling
Most API traffic today is machine to machine: services, jobs, partners, mobile backends and increasingly AI agents. The design must say which kind of caller each operation expects and how it proves who it is.
- Service and partner callers: OAuth 2.0 client credentials with short-lived, audience-restricted tokens, or mutual TLS for high-trust links. See the Machine-to-Machine OAuth (Client Credentials) Guide and the mTLS Guide.
- API keys: acceptable for identifying a client and metering, weak as the only protection for sensitive data (OWASP REST Security Cheat Sheet). If you issue keys, design for prefixing, hashing at rest, scoping, expiry and rotation from the start. See the API Key Management Guide and API Keys vs OAuth Tokens Guide.
- Token validation: select the verification algorithm from server configuration, never from the token header, reject
none, and validateiss,audandexp. See the JWT Security Guide. - Headers set by intermediaries: if a gateway or backend-for-frontend adds identity headers such as
X-User-IdorX-Forwarded-For, the design must strip any client-supplied copy (ASVS V4.1.3), or a caller can claim any identity. - Highly sensitive transactions: consider per-message signatures on top of TLS for payment or approval requests that cross several systems (ASVS V4.1.5). For webhooks, see the Webhook Secrets Security Guide.
Data exposure: request and response models
Mass assignment and excessive data exposure are two sides of the same habit: binding the database entity directly to the wire. The fix is a design rule, not a review comment.
- Input models per operation: a
CreateUserRequestcontains name and email; it does not containrole,isVerified,tenantIdorcreditLimit. ASVS V15.3.3 requires limiting allowed fields per controller and action so a client cannot set a field it was never meant to set. - Output models per audience: the public profile, the owner's view and the administrator's view are different schemas. Do not return a field and rely on the front end to hide it; OWASP API3 describes exactly that failure.
- Filtering, sorting and field selection: allow-list the fields that may be used in
sort,filterandfieldsparameters. A sort on an unexposed field (such as a password reset token) can leak its value one comparison at a time.
Treat data from upstream APIs the same way: validate and map partner responses into your own models instead of passing them through. See the Input Validation and Output Encoding Guide.
Errors, status codes, idempotency and concurrency
A single error model
RFC 9457 (which obsoletes RFC 7807) defines application/problem+json with the members type, title, status, detail and instance. Its security considerations warn against exposing internals such as stack dumps. Adopt it across all APIs, put a correlation ID in instance or an extension member, and log the internal detail server-side under that ID. See the Secure Error Handling and Logging Guide.
| Status | Use it for | Design note |
|---|---|---|
| 400 | Malformed request or schema violation | Name the failing field, never echo raw input into HTML |
| 401 | Missing or invalid credentials | Same response for unknown and expired tokens |
| 403 or 404 | Authenticated but not permitted | Returning 404 for objects the caller may not see avoids confirming that they exist; pick one rule and apply it everywhere |
| 409 | State conflict, concurrent retry | Used with idempotency keys and ETags |
| 422 | Semantically invalid request, idempotency key reused with a different payload | Business rule failures |
| 429 | Rate or quota exceeded | Include Retry-After |
| 500 | Unexpected server error | Generic message, correlation ID only |
Idempotency and concurrency
Retries are where duplicate payments and double refunds come from. The IETF HTTPAPI working group draft for the Idempotency-Key request header (draft 08, still an Internet-Draft in October 2026) recommends a unique, UUID-like key per operation, optional fingerprinting of the payload, a published expiry policy, 400 when a required key is missing, 422 when a key is reused with a different payload and 409 when a retry arrives while the original is still in progress. Design POST operations that move money or create orders to require it. For updates, use ETag and If-Match so that a client cannot overwrite a change it never saw.
Abuse resistance, limits and business flows
ASVS V2.4.1 requires anti-automation controls against excessive calls that could lead to data exfiltration, quota exhaustion or resource exhaustion.
- Per-identity limits: limit by client ID or user, not only by IP address, and return 429 with
Retry-After. - Per-flow limits: sign-up, password reset, voucher redemption, checkout and search each get their own thresholds based on realistic human or partner behaviour.
- Cost-based limits: expensive operations (exports, report generation, LLM calls, GraphQL queries) cost more units than simple reads.
The T-Mobile API breach is the cautionary example: data on about 37 million accounts was retrieved through a single API over roughly six weeks before detection. T-Mobile has not disclosed the root cause, but per-client volume limits and alerting on bulk lookups are the design controls that shorten that window. See the Rate Limiting and API Abuse Guide.
Versioning, deprecation and inventory
- Version explicitly: a path or header version, with security fixes applied to every supported version, not only the newest.
- Announce the end: RFC 9745 (March 2025) defines the
Deprecationresponse header, a structured date that can be in the future or past, and adeprecationlink relation to migration documentation. Pair it with theSunsetheader from RFC 8594; the Sunset date must not be earlier than the Deprecation date. - Turn it off: after sunset, return 410 Gone and remove the route, the credentials and the infrastructure. Monitor for callers still hitting the old version before the date.
- Register every version and environment: owner, data classification, exposure (internet, partner, internal), authentication scheme and sunset date, kept in the same catalogue as the contract. Non-production hosts with production data are inventory too.
Rolling secure design into delivery
- Write the standard (weeks 1 to 4): a two-page API design standard covering the defaults in this guide, a problem details schema, a naming and versioning rule, and an approved list of authentication schemes. Base it on ASVS 5.0 chapters V4 and V8.
- Template it: publish service templates and shared middleware where authentication, scoped data access, validation, limits and error handling are already wired.
- Threat model new APIs at design review: one session per new API or major change, focused on who calls it, what objects it exposes and which flows are worth abusing. The NCSC API guidance also recommends threat modelling tailored to the specific design. See the Threat Modelling Guide.
- Lint and test in the pipeline: contract linting as a blocking check; contract tests and negative authorisation tests (user A requests user B's object, a read-only token calls a write operation) as part of the normal test suite.
- Pilot, then enforce: run gateway schema validation in log-only mode on one API for two weeks, review what would have been rejected, fix the contract or the client, then switch to blocking. Roll back by reverting the gateway policy, not by removing the schema.
Small teams get most of the value from a framework template, a contract linter and negative authorisation tests. Large organisations also need a central API catalogue, a design review board for public and partner APIs, and a policy engine for shared authorisation rules. See the Secure Software Development Lifecycle (SSDLC) Guide.
Detection and measurement
- Log what design decisions protect: authorisation denials with subject, action and resource; validation failures; token validation errors; 429s; calls to deprecated versions.
- Alert on enumeration: one identity reading many distinct object IDs, sequential ID patterns, or a spike in 403 or 404 responses from one client.
Metrics that show the design standard is working:
- Percentage of APIs with a linted contract in the catalogue (target 100% of internet-facing APIs).
- Percentage of operations with explicit security and scopes, and the count of approved public exceptions.
- Percentage of APIs with negative authorisation tests in CI.
- Number of API versions past their sunset date still receiving traffic (target zero).
Common failure patterns
- Authorisation by obscurity: relying on UUIDs or hidden routes instead of an ownership check. Unguessable is not unauthorised.
- Entity binding: one model for database, request and response, so new database columns become writable and readable without anyone deciding they should be.
- Trusting the gateway alone: the gateway checks scopes, the service assumes that is enough, and object ownership is never checked.
- Lookup APIs with no volume controls: an endpoint designed for single lookups is used for bulk extraction, as the T-Mobile API breach illustrates.
- Versions that never die: v1 kept for one partner, never patched, and missing from the inventory.
Framework mapping
| Framework | Reference | How secure API design supports it |
|---|---|---|
| OWASP ASVS 5.0 | V2.2.1, V2.2.2, V2.4.1, V4.1.1 to V4.1.5, V4.3.1, V8.2.1 to V8.2.3, V8.3.1, V8.3.3, V15.3.3 | Verifiable design and code requirements for validation, HTTP handling, GraphQL, authorisation and mass assignment |
| NIST CSF 2.0 | PR.PS-06, PR.AA-05, ID.AM-02 | Secure development practice, least-privilege access, inventory of services |
| NIST SP 800-53 Rev. 5 | SA-8, SA-11, AC-3, AC-6, SI-10, SC-8, SC-5, CM-7 | Engineering principles, developer testing, access enforcement, input validation, transport protection, DoS protection, least functionality |
| CIS Controls v8.1 | Control 16 Application Software Security | Secure design principles and application security testing |
| ISO/IEC 27001:2022 Annex A | A.8.25, A.8.26, A.8.27, A.8.28 | Secure development life cycle, application security requirements, secure architecture and engineering principles, secure coding |
| MITRE ATT&CK | T1190 Exploit Public-Facing Application | Design flaws in internet-facing APIs are a common initial access route |
Practitioner checklist
- Every API has a contract in the catalogue with a global security requirement, per-operation scopes and strict schemas.
- Contract linting blocks missing security, unbounded strings and arrays, and permissive
additionalPropertieson request bodies. - Data access is scoped by the caller's tenant and permissions in a shared layer; client-supplied owner IDs are avoided.
- Request and response models are separate per operation; writable and readable fields are allow-listed.
- Downstream calls carry the end user's identity where per-user rules apply.
- All APIs return RFC 9457 problem details with generic messages and a correlation ID.
- Payment and order creation operations require an
Idempotency-Key; updates useETagandIf-Match. - Per-identity and per-flow limits are defined for every sensitive business flow.
- Every version has an owner and a sunset date, announced with
DeprecationandSunsetheaders. - Negative authorisation tests run in CI for every API.
Standards and references
- OWASP: API Security Top 10 2023
- OWASP: Application Security Verification Standard 5.0
- OWASP: REST Security Cheat Sheet
- IETF: RFC 9457 Problem Details for HTTP APIs
- IETF: RFC 9745 The Deprecation HTTP Response Header Field
- IETF: The Idempotency-Key HTTP Header Field (Internet-Draft)
- OpenAPI Initiative: OpenAPI Specification
- NCSC: Securing HTTP-based APIs
Related NHI Mgmt Group resources: API Security Guide · Top 10 API Security Risks · API Gateway Security Guide · API Penetration Testing Guide · Content Security Policy Guide