Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
APIs present a distinct threat-modeling problem from traditional web applications: there is no browser enforcing same-origin policy, no server-rendered page hiding internal object identifiers, and every endpoint is, by design, a documented, machine-callable entry point — often published as an OpenAPI (formerly Swagger) specification that doubles as both integration documentation and an attacker’s reconnaissance map. Threat modeling an API means walking that specification systematically and asking, for every operation, what trust the client is assumed to have and what happens when that assumption is violated — because unlike a traditional app where a UI might discourage a user from trying an invalid action, an API client can call any documented (or undocumented but guessable) endpoint with any parameter value, directly.
The OWASP API Security Top 10 exists precisely because API vulnerabilities cluster differently from the classic OWASP web Top 10: Broken Object Level Authorization (BOLA) and Broken Function Level Authorization (BFLA) dominate real-world API findings far more than injection does, because APIs expose object identifiers and administrative operations directly and explicitly, and authorization logic has to be re-verified on every single operation rather than inherited from a page-level access check. A threat model built from the OpenAPI spec systematically surfaces exactly these gaps before the API ships.
Attack Prerequisites
To threat-model an API effectively, a few inputs need to exist first:
- A reasonably complete API specification (OpenAPI/Swagger, or reconstructed from the code if none exists) listing every operation, parameter, and expected response — an incomplete spec produces an incomplete threat model.
- Clarity on identity and trust levels — which operations are unauthenticated, which require a user token, which require an admin/service-to-service credential — mapped explicitly per endpoint, not assumed globally.
- Knowledge of the object/resource ownership model — for every endpoint returning or modifying an object, who is supposed to be allowed to touch *that specific instance*, which is the question BOLA analysis depends on entirely.
- Awareness of rate/volume-sensitive operations — anything that sends email/SMS, performs a financial transaction, or is expensive to compute, since APIs are especially exposed to automation and mass-assignment consequences that a human-driven UI would naturally throttle.
How It Works
A practical API threat model walks the specification operation by operation rather than the system architecture as a whole, because API authorization bugs are almost always *local* to a single endpoint’s implementation rather than a systemic design flaw. For every operation, the review asks the same fixed set of questions: what identity is required, what object does this operation act on and does the code verify the caller *owns or is entitled to that specific object* (not just that they are authenticated), what happens with an unexpected value in each parameter (a negative page size, an oversized array, an unexpected enum), and — separately from object-level checks — is this specific *function* restricted to the right role, since a regular user calling an admin-only operation with a valid but insufficiently-privileged token is a distinct bug class (BFLA) from an authorized user reaching someone else’s object (BOLA).
Mass assignment is a recurring API-specific pattern worth calling out on its own: many frameworks bind request JSON directly onto a model object (user.update_attributes(request.json) or equivalent), which means any field on that model — including ones the client was never meant to set, like is_admin or account_balance — becomes writable simply by including it in the request body, regardless of whether the API documentation mentions that field at all. This is invisible from the OpenAPI spec if the spec only documents intended fields, which is exactly why the threat model has to look at the underlying model/serializer, not only the published contract.
Rate and resource abuse rounds out the API-specific threat surface: operations with no volumetric limit are trivially automatable in a way a browser-driven UI discourages by friction alone — password reset, OTP delivery, search/export endpoints, and any endpoint that fans out into an expensive downstream query are all classic targets for enumeration, credential stuffing, or straightforward denial-of-service via legitimate-looking but excessive call volume.
Practical Example / Configuration
An OpenAPI operation, annotated with the trust-boundary and authorization questions a threat-model pass should ask of it — the comments are the threat-modeling artifact, not standard OpenAPI syntax, but are commonly kept in a companion review doc keyed to the operationId:
paths:
/accounts/{accountId}/transactions:
get:
operationId: listTransactions
security:
- bearerAuth: []
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
- name: pageSize
in: query
schema: { type: integer, default: 25 }
responses:
'200':
description: List of transactions
# THREAT MODEL NOTES (listTransactions):
# - BOLA: does the handler verify accountId belongs to the bearer token's
# subject, or only that SOME valid token was presented? -> TEST
# - BFLA: n/a (read op, no elevated role implied)
# - Resource abuse: pageSize has no documented max -> can a client request
# pageSize=999999999 and force an expensive full-table scan? -> TEST
# - Mass assignment: n/a (GET, no request body)
YAMLThe corresponding abuse-case table, the artifact that turns the annotated spec into backlog-trackable findings:
- Operation:
listTransactions· Abuse case: authenticated user A requests/accounts/{B's accountId}/transactionsusing their own valid token · Class: BOLA · Expected control: 403/404 unlessaccountIdbelongs to the token subject · Status: needs test. - Operation:
PATCH /users/{id}· Abuse case: a standard user includes"role": "admin"in the request body, relying on mass assignment in the model binder · Class: Mass assignment / BFLA · Expected control: explicit allowlist of client-settable fields in the serializer · Status: unmitigated. - Operation:
POST /password-reset· Abuse case: unauthenticated client sends thousands of reset requests to enumerate valid emails via response-time or message differences · Class: Resource abuse / enumeration · Expected control: uniform response + rate limit per IP and per target address · Status: mitigated (rate-limited).
Walkthrough / Implementation
A repeatable process for threat-modeling a new or existing API from its spec:
1. Pull every operation from the OpenAPI doc (or generate one from code if
missing: many frameworks can export it - e.g. FastAPI's /openapi.json).
2. For each operation, fill a fixed row: auth requirement, object touched,
ownership check present (Y/N/unknown), role check present (Y/N/unknown),
rate limit present (Y/N), request-body fields vs. serializer allowlist.
3. Flag every 'unknown' or 'N' for a required check as a finding; convert
to a concrete abuse case with actor/goal/expected-control, as above.
4. Prioritize by data sensitivity of the object touched, not alphabetical
spec order - a BOLA on a transactions endpoint outranks one on a
display-preferences endpoint.
5. Turn each finding into an automated test (an authorization test suite
that iterates every object-returning endpoint with a second, unrelated
user's token and asserts a 403/404) so the finding cannot silently
regress after the fix.
TEXTA minimal automated BOLA regression check, run in CI against a staging environment with two seeded test users:
# Iterate every {resource}/{id} GET operation from the OpenAPI spec and
# assert user B cannot read an object owned by user A.
for op in object_level_get_operations(openapi_spec):
resp = client.get(op.path.format(id=user_a.owned_id),
headers=auth_header(user_b_token))
assert resp.status_code in (403, 404), (
f"BOLA: {op.operation_id} leaked user A's object to user B")
PythonNote: BOLA and BFLA checks cannot be found reliably by SAST or DAST — both require knowing which object ‘belongs’ to which caller, a fact that lives in business logic and test data, not in a static pattern. This is the strongest reason to threat-model APIs explicitly rather than relying on generic scanning to cover them.
Note: Undocumented or deprecated endpoints (an old
/v1/API left reachable alongside/v2/, or debug endpoints never removed from staging) are a recurring API-specific finding precisely because they are absent from the current spec and therefore invisible to a spec-driven threat model — pair the OpenAPI-based review with a live discovery pass (route enumeration from the router/gateway config, not just the published doc).
Detection and Defense
Concrete controls that address the API-specific threat surface above:
- Enforce object-level authorization on every operation that returns or mutates a specific resource, verified against the caller’s identity, not just checked for “is authenticated” — this is the single highest-value control given how dominant BOLA is in real API findings.
- Use an explicit field allowlist for request-body binding (a DTO or explicit serializer schema) rather than binding raw JSON onto a model, to eliminate mass assignment as a class.
- Apply per-role function-level checks separately from object-level checks — an endpoint can correctly scope to “your own object” and still be missing a role check that should have blocked a non-admin caller entirely.
- Rate-limit and monitor by API key/token, not just by IP, since distributed or credentialed abuse defeats IP-based limits alone.
- Keep the OpenAPI spec authoritative and CI-validated against the actual route table, so undocumented/deprecated endpoints cannot silently drift out of the threat model’s visibility.
Real-World Impact
The OWASP API Security Top 10 (first published 2019, updated 2023) exists as a separate list from the general OWASP web Top 10 specifically because industry API-focused assessments and bug-bounty data consistently show BOLA as the single most common and highest-impact API finding, ahead of injection or XSS, which barely apply to a JSON-only API surface in the first place. Numerous large-scale breaches and bug-bounty disclosures — commonly involving mobile-app backends and B2B integration APIs — have traced back to exactly this pattern: an endpoint that authenticates the caller correctly but never checks whether the caller is entitled to the specific object ID they supplied.
Conclusion
API threat modeling works best as a systematic, spec-driven pass rather than an architecture-level diagram, because API vulnerabilities are overwhelmingly local to individual operations: does this endpoint verify object ownership, is the function-level role check separate and present, and can the request body set fields the client was never meant to touch. Walk the OpenAPI spec operation by operation, turn every gap into a trackable abuse case, and back the fix with an automated regression test so BOLA and BFLA findings cannot quietly return.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments