Threat Modeling APIs: From OpenAPI to Abuse Cases

Threat Modeling APIs: From OpenAPI to Abuse Cases - article cover image Security
Time it takes to read this article 7 minutes.

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)
YAML

The 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}/transactions using their own valid token · Class: BOLA · Expected control: 403/404 unless accountId belongs 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.
TEXT

A 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")
Python

Note: 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

Copied title and URL