CyberNugget

L39 · The OWASP API Security Top 10

Module 6: API Security · ~20 min · Lesson 39 of 71

🎯 What you'll learn

  • Why APIs got their own OWASP list, separate from the web Top 10 (L29)
  • The ten risks of the 2023 edition, grouped into four questions you can remember
  • What changed from the 2019 edition, and what was dropped (but not solved)
  • How to review one endpoint against the whole list in ten minutes

🔥 Why this matters

Picture a customer portal that passes its web pen test: no XSS, no injection, clean headers. But the real product is api.example.com, called by the portal, the mobile app, two partners and an Airflow job. Nobody asked whether /v1/customers/C-2002/transactions checks that you are C-2002, or why /v0 is still routed. Those are API questions, and they need an API list.

1. Why APIs got their own list

A classic web app decides what you see on the server and sends finished HTML. An API-first app sends raw JSON objects, and the client decides what to show.

 SERVER-RENDERED WEB APP          API-FIRST APP
 browser                          any client: SPA, mobile app,
   │ 1. GET /statement              partner, curl, a script
   ▼                                │ 1. GET /v1/accounts/ACC-1001
 server loads data, filters         ▼
 it, renders HTML                 API returns the JSON object
   │ 2. HTML with 3 fields          │ 2. JSON with all 30 fields
   ▼                                ▼
 user sees what the               client hides 27 fields;
 server chose                     an attacker reads them all

That shift moves the risk:

  • The client is attacker-controlled code. Anything the app hides, a script can still read or send.
  • Every request names an object. IDs in paths and bodies are an API's normal grammar, so each one needs an authorization check.
  • More surface. Many endpoints and versions, and callers that never touch a browser.
  • APIs consume APIs. Your service is also a client of vendors and other teams.

Browser bugs like XSS and CSRF matter less here. Authorization, business logic and inventory matter more, and scanners are weak at those: a scanner can't know who should own ACC-1001.

The OWASP API Security Top 10 first came out in 2019. The current edition is 2023, and IDs work like the web list: API1:2023. One difference from L29: OWASP's public call for data got no submissions, so the 2023 list was built from the project team's experience, review by API security specialists, and public comments on a release candidate. Treat it as expert consensus, not statistics.

2. The ten risks, as four questions

Learn it as four questions to ask of any endpoint:

 1. WHO ARE YOU?
    API2   Broken Authentication

 2. WHAT MAY YOU TOUCH?                       (authorization)
    API1   Broken Object Level Authorization
    API3   Broken Object Property Level Authorization
    API5   Broken Function Level Authorization

 3. HOW MUCH, HOW OFTEN?
    API4   Unrestricted Resource Consumption
    API6   Unrestricted Access to Sensitive Business Flows

 4. WHAT ELSE IS EXPOSED OR TRUSTED?
    API7   Server Side Request Forgery
    API8   Security Misconfiguration
    API9   Improper Inventory Management
    API10  Unsafe Consumption of APIs

Three of the ten are authorization, at three levels. One request can fail all three:

 PATCH /v1/admin/accounts/ACC-1002    {"creditLimit": 900000}
       └───┬────┘          └───┬───┘   └──────────┬──────────┘
           │                   │                  │
    FUNCTION (API5)     OBJECT (API1)      PROPERTY (API3)
    may this caller     is ACC-1002        may this caller
    use admin           this caller's?     set creditLimit?
    operations?

The tour, mapped to a typical API stack:

Risk What it looks like in practice Course
API1 Broken Object Level Authorization (BOLA) C-2001's token reads /v1/customers/C-2002/transactions L34, L40
API2 Broken Authentication JWT aud not checked; no attempt limit on POST /v1/otp/verify; API keys in URLs L21–L25
API3 Broken Object Property Level Authorization (BOPLA) Response includes fraudScore; a PATCH accepts "kycStatus": "VERIFIED" L41
API4 Unrestricted Resource Consumption ?limit=100000; a 50 MB body; each call triggers a paid SMS or bureau lookup L42
API5 Broken Function Level Authorization (BFLA) A customer token calls POST /v1/admin/fees/waive L40
API6 Unrestricted Access to Sensitive Business Flows Bots test stolen cards on "add card", or farm sign-up bonuses Deep dive 1
API7 Server Side Request Forgery "Import statement from URL" reaches 169.254.169.254 L35
API8 Security Misconfiguration Actuator routed through Kong; permissive CORS; stack traces in 500s L37, L38
API9 Improper Inventory Management /v0 still routed; a -dev host holding prod data Try it
API10 Unsafe Consumption of APIs A KYC vendor's response stored unchecked; its redirects followed Deep dive 2

Two pairs people mix up:

  • API1 vs API5 is object vs function: "may I touch this account?" vs "may I call this operation at all?" L40 is about this pair.
  • API4 vs API6 is cost vs purpose. API4 is what a request costs you (CPU, memory, money); API6 is what many cheap, valid requests do to the business. Rate limits help both, and fix neither.

3. What changed from 2019 to 2023

 2019                                  2023
 API1 BOLA ─────────────────────────▶ API1 BOLA (unchanged)
 API2 Broken User Authentication ───▶ API2 Broken Authentication
 API3 Excessive Data Exposure ──┬───▶ API3 Broken Object Property
 API6 Mass Assignment ──────────┘          Level Authorization
 API4 Lack of Resources & Rate ─────▶ API4 Unrestricted Resource
      Limiting                             Consumption
 API5 BFLA ─────────────────────────▶ API5 BFLA (unchanged)
 API7 Security Misconfiguration ────▶ API8 Security Misconfig.
 API9 Improper Assets Management ───▶ API9 Improper Inventory
                                           Management
 API8 Injection ────────────────────▶ (dropped)
 API10 Insufficient Logging & ──────▶ (dropped)
       Monitoring
                                 NEW ▶ API6 Sensitive Business Flows
                                 NEW ▶ API7 SSRF
                                 NEW ▶ API10 Unsafe Consumption
  • Merged by root cause. Reading a field you shouldn't and writing one are the same missing check at the property level (L41).
  • Renamed to name the harm. "Lack of rate limiting" named a control; "unrestricted resource consumption" names the risk, including the bill for paid third-party calls.
  • Dropped is not solved. The release notes don't say why Injection and Logging left. The common reading is that they aren't API-specific and the web Top 10 covers them (A05:2025 and A09:2025). The NoSQL injection checks from L31 still apply to every API.

🧪 Worked example: one endpoint, ten questions

A pen tester calls a (fictional) transactions-svc on EKS, behind Kong, with customer C-2001's token:

GET /v1/customers/C-2002/transactions?limit=100000 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...   (sub: C-2001)

HTTP/1.1 200 OK
{"items": [{"txId": "TX-90311", "amount": -42.10,
            "merchant": "Example Coffee",
            "cardNumber": "4111111111111111",
            "fraudScore": 0.12}, ...],
 "count": 100000}

Walk the list, one question each:

Risk Question Finding
API1 Is C-2002 the caller? No. BOLA: the path ID isn't checked against the token
API2 Is the token verified (signature, iss, aud, exp)? Yes, in Kong and in Spring Security
API3 Does the client need every field? No. Full card number and fraudScore leak
API4 Is limit capped? No. 100,000 rows in one call
API5 Do other methods exist on this path? DELETE .../transactions/{txId}, meant for ops, accepts customer tokens
API6 Is this a sensitive business flow? No; reading history isn't abusable in bulk beyond API1/API4
API7 Does it fetch URLs? No
API8 What does an error look like? ?from=bad returns a Java stack trace
API9 Which versions are live? /v0/customers/... is still routed in Kong, with no owner
API10 Does it call other APIs? Merchant logos from a vendor, rendered unchecked

Six findings, and a web scanner would likely have flagged only the stack trace. The fixes land in different layers: the query and response DTO (API1, API3), a limit cap (API4), a route rule (API5), and a Kong cleanup (API9). L40 to L43 take them one by one.

🛠 Try it (optional, ~5 min)

Inventory (API9) starts with what you publish. This script flags OpenAPI operations worth a review. Save a sample spec as spec.json (made-up routes):

{"openapi": "3.0.3",
 "security": [{"bearer": []}],
 "paths": {
  "/v1/customers/{customerId}/transactions": {"get": {}},
  "/v1/admin/fees/{feeId}/waive": {"post": {}},
  "/v0/customers/{customerId}": {"get": {"deprecated": true}},
  "/v1/health": {"get": {"security": []}}}}

Save this as api_inventory.py:

import json, sys

spec = json.load(open(sys.argv[1]))
default = spec.get("security", [])
METHODS = {"get", "post", "put", "patch", "delete"}
for path, ops in spec["paths"].items():
    for method, op in ops.items():
        if method not in METHODS:
            continue
        flags = []
        if not op.get("security", default):
            flags.append("API2:no-auth")
        if "{" in path:
            flags.append("API1:id")
        if "admin" in path or "internal" in path:
            flags.append("API5:privileged")
        if op.get("deprecated"):
            flags.append("API9:deprecated")
        print(f"{method.upper():6} {path:39} {' '.join(flags)}")

Run python3 api_inventory.py spec.json:

GET    /v1/customers/{customerId}/transactions API1:id
POST   /v1/admin/fees/{feeId}/waive            API1:id API5:privileged
GET    /v0/customers/{customerId}              API1:id API9:deprecated
GET    /v1/health                              API2:no-auth

Then run it on your own service's spec, on your own machine. Every API1 line needs an ownership check, every API5 line a role rule, and every no-auth line a good reason. Then ask the real API9 question: which gateway routes (Kong routes, in the example stack) aren't in any spec? Those are shadow APIs.

🧠 Key takeaways

  • APIs hand raw objects to a client the attacker controls, so authorization and business logic dominate the risk, and scanners are weak at both.
  • Remember the 2023 list as four questions: who are you, what may you touch, how much and how often, and what else is exposed or trusted.
  • Authorization appears three times: object (API1), property (API3), function (API5).
  • 2023 merged excessive data exposure and mass assignment into API3, and added business flows, SSRF and unsafe consumption. Injection was dropped from the list, not from reality.
  • Review an endpoint by walking all ten; the fixes land in different layers.

📖 Words to know

Term Meaning
OWASP API Security Top 10 OWASP's awareness list of the ten most critical API risks; current edition 2023
BOLA Broken Object Level Authorization (API1): acting on an object that isn't yours
BOPLA Broken Object Property Level Authorization (API3): reading or writing fields you shouldn't
BFLA Broken Function Level Authorization (API5): calling an operation your role shouldn't use
Sensitive business flow A legitimate feature that hurts the business when automated at scale
Shadow API An endpoint that's live but not in any inventory or spec
Zombie API An old version that should be retired but still answers
Unsafe consumption Trusting responses from third-party or internal APIs without validating them

⏭ Next time

L40 goes deep on the top pair, BOLA and BFLA: the patterns that survive a first fix, and a deny-by-default design that makes both hard to forget.

Deep dive 1

Business-flow abuse (API6): when every request is valid

Most of the list is about bugs. API6 is different: the feature works exactly as designed, and that's the problem. OWASP's own examples include bots buying up limited stock and farming referral credits. Each request is authenticated, authorized and well-formed. The harm is in the volume and the intent.

What makes a flow "sensitive"

A flow is sensitive when using it at machine speed costs you money, stock, reputation or a regulator's attention. Deciding which flows qualify is a business decision first, which is why OWASP lists "identify the flows" as the business half of the fix. Fintech has plenty:

Flow Abuse What it costs
Add or verify a card Card testing: bots check stolen card numbers with small authorizations Processing fees, chargebacks, card-network scrutiny
Send an SMS one-time code SMS pumping (toll fraud): codes sent to premium or high-cost numbers the attacker profits from The SMS bill
Open an account with a bonus Bonus and referral farming with fake or synthetic identities The bonuses, plus KYC costs
Get a quote or rate Scraping your pricing Competitive advantage
Micro-deposit account check Testing which bank accounts are real Fraud downstream

Worked example: SMS pumping on account opening

Step 1 of a (fictional) account-opening flow is POST /v1/onboarding/otp with a phone number. It needs no login, because the customer doesn't exist yet:

 1. bot ──POST /v1/onboarding/otp {"phone": "+XX..."}──▶ Kong
 2. Kong: per-IP limit ok (bot rotates 5,000 residential IPs)
 3. onboarding-svc ──"send code"──▶ SMS provider
 4. provider ──SMS──▶ premium-rate numbers in a country
                      you don't serve
 5. carrier pays the number's owner a share; you pay the bill

 every request: valid JSON, valid number, under rate limits

Nothing in steps 1 to 3 is a bug. The fix is a set of business rules on the flow, close to the code that spends the money:

  • Only allow what you serve. If you onboard US customers, reject non-US numbers before sending.
  • Limit on business keys, not just IPs. Codes per phone number, per number prefix, per device, and per hour across the whole flow (L42 covers the mechanics).
  • Enforce the order of steps. Issue a short-lived flow token on the first page; the OTP call requires it, so a bot can't call step 1 in a loop without fetching step 0.
  • Watch the cost metric. Alert when SMS spend per hour leaves its normal band (L62). This is how most teams notice first.

OWASP's engineering advice adds device fingerprinting, human detection, spotting non-human timing, and considering blocks on Tor exit nodes and known proxies. Be careful with that last one in banking: some legitimate customers use privacy tools, so prefer step-up checks over hard blocks.

Why gateways and WAFs miss it

A WAF looks for malicious content (L12), and there is none here. A per-IP rate limit at Kong sees 5,000 polite clients. Only the service knows that 4,000 different numbers in one country prefix were sent codes in ten minutes. That's why API6 controls usually live in the service or a fraud platform, with the gateway as a coarse first layer (L43).

🧠 Extra takeaways

  • API6 risks are features working as designed, abused at machine speed.
  • Naming the sensitive flows is a business decision; list them in your threat model (L05).
  • Limit on business keys (phone, card, device, account), not just IP addresses.
  • Watch the cost or business metric; it often moves before any security alert.
Deep dive 2

Unsafe consumption (API10): the APIs you call are input too

Teams validate what customers send them, then trust whatever a vendor's API sends back. API10 is that gap. If an attacker can't break your API directly, they can try to break, impersonate or manipulate something your API trusts: a KYC vendor, an FX rate feed, a partner's webhook, or another internal team's service.

What goes wrong

OWASP's API10 guidance boils down to a few failure modes:

  • Following redirects blindly. The vendor (or someone who has compromised it) answers with a redirect to an attacker's host, and your client follows it, possibly re-sending sensitive data.
  • Storing data without validating it. A malicious string lands in your database and becomes injection or XSS later (second-order, as in L30's deep dive).
  • No limits. No timeout, no size cap: one slow or huge response ties up your workers (API4 in reverse).
  • Weak transport. verify=False "just for this vendor", or plain HTTP inside the VPC.
  • Trusting the vendor's security posture without ever assessing it (L13's Target breach began at a third party).

Worked example: an Airflow task that pulls FX rates

A daily Airflow task fetches exchange rates from a (fictional) vendor and writes them to MongoDB, where payments-svc uses them to convert transfers.

# VULNERABLE
def load_rates():
    resp = requests.get("https://rates.example.net/v2/latest")
    rates = resp.json()["rates"]           # any shape, any size
    db.fx_rates.insert_one({"date": today(), "rates": rates})

Four problems: Python requests has no default timeout, it follows redirects on GET by default, the body size is unbounded, and a rate of 0, -1 or "1e9" goes straight into payments math.

# FIXED
from decimal import Decimal
from pydantic import BaseModel, condecimal

Rate = condecimal(gt=Decimal("0.0001"), lt=Decimal("100000"))

class RatesResponse(BaseModel):
    base: str
    rates: dict[str, Rate]

def load_rates():
    resp = requests.get("https://rates.example.net/v2/latest",
                        timeout=(3, 10),            # connect, read
                        allow_redirects=False)     # no surprises
    resp.raise_for_status()                        # 3xx -> error
    if len(resp.content) > 256_000:
        raise ValueError("rates response too large")
    data = RatesResponse.model_validate_json(resp.content)
    if data.base != "USD" or not {"EUR", "GBP"} <= data.rates.keys():
        raise ValueError("unexpected rates payload")
    check_daily_move(data.rates, max_change=Decimal("0.05"))
    db.fx_rates.insert_one({"date": today(),
                            "rates": {k: str(v) for k, v
                                      in data.rates.items()}})

Notes on the fix:

  • raise_for_status() raises on 4xx and 5xx only. With redirects off, a 3xx would pass silently, so in real code also check resp.status_code == 200.
  • The size check here runs after download. For a hard cap, stream the response (stream=True) and stop reading past the limit.
  • check_daily_move (your own function) compares against yesterday's rates and fails the task if any currency moved more than 5%. A failed task pages someone; a silently wrong rate moves money.
  • pydantic v2 syntax is shown (model_validate_json); v1 used parse_raw.

A checklist for every integration

Check Why
TLS verified, ideally to a pinned host name list No one in the middle (L10)
Timeouts, size caps, retries with backoff A sick vendor can't take you down
Redirects off, or an allow-list of hosts No data sent to a surprise host
Schema validation with business bounds Bad data fails loudly, at the edge
Encode on output, parameterize on storage Vendor strings can't become code (L30–L32)
Credentials scoped to this vendor, rotated A leak at one vendor stays small (L19)
A named owner and a review date Vendor integrations belong in your inventory (API9)

🧠 Extra takeaways

  • Data from an API you call is untrusted input, even from a paying vendor or another internal team.
  • Set timeouts, size caps and redirect rules explicitly; library defaults are tuned for convenience.
  • Validate shape and business sense (bounds, day-over-day change) before data reaches money.
  • Fail the job loudly rather than storing something wrong quietly.

The quiz for this lesson and the Tests section need JavaScript.