API Security: What Your WAF Can't See
Your customer-facing API returns full account details for any account number submitted by any authenticated user. No ownership check. Just authentication. A security researcher writes a 12-line Python script. Iterates account IDs from 1 to 500,000. Downloads every customer’s balance, transaction history, and personal details. Takes under three hours. The endpoint has been live for years. It passed multiple pen tests. No scanner flagged it because every single request carried a valid JWT.
Valid badge. Wrong room. The front door checked the badge and waved them through. No room in the building checked whether this person was supposed to be there.
BOLA (Broken Object Level Authorization) sits at number one on the OWASP API Security Top 10 for a reason most teams find uncomfortable: finding it requires business context no automated tool has. A WAF sees a valid token, valid endpoint, valid response. Everything looks correct at the network layer. The business logic failure, user A reading user B’s invoice, is invisible to every security tool in the stack. The perimeter alarm didn’t go off because the intruder had a key.
- BOLA is the #1 API vulnerability because no scanner can find it. Every request is authenticated, structurally valid, and uses the right HTTP method. The flaw is invisible at the network layer.
- Authorization belongs at the data layer, not the gateway.
WHERE account_id = :user_account_idon every query. No exceptions. No shortcuts. - Mass assignment lets attackers escalate privileges by sending fields your API doesn’t expect. Allowlist input fields explicitly.
- API rate limiting should target per-user patterns, not just global throughput. 100 sequential requests to
/accounts/{id}with incrementing IDs is an attack, not traffic. - Shadow APIs are the endpoints nobody documented but traffic analysis reveals. Every undocumented endpoint is unmonitored attack surface. Doors nobody put on the floor plan.
The Authorization Problem
BOLA consistently ranks as the most critical API vulnerability. Easy to exploit for any authenticated user. Nearly invisible to automated scanning. Most teams think they’re immune until the pen test proves otherwise.
The pattern is dead simple. An API endpoint accepts a resource identifier: GET /api/invoices/84721. The server validates the token, confirms the user is logged in, and returns the invoice. What it never checks: does the authenticated user have any relationship to invoice 84721? An attacker who is a legitimate customer just iterates invoice IDs and reads other customers’ billing data. No zero-days. No fancy tooling. Just a for loop. The lock on the front door works. Nobody checked the rooms.
Fixing BOLA requires authorization at the data access layer. The service checks that the authenticated identity has a real relationship to the requested resource. In practice: your query needs a WHERE user_id = $authenticated_user clause, or your ORM scopes by tenant automatically. This lives in application code. The gateway can’t make this call because it doesn’t know the business rules. The front desk can check your badge is real. Only the department head knows whether you belong in the meeting.
Don’t: Rely on the API gateway for resource-level authorization. The gateway validates tokens and rate limits. It has no idea which invoice belongs to which user. Asking the bouncer to do the accountant’s job.
Do: Build authorization middleware that every endpoint uses by default. The developer must declare the ownership relationship (e.g., @authorize(resource="invoice", owner_field="user_id")). Make the insecure path require effort. Make the secure path the default.
The zero trust architecture guide covers how this fits into a broader authorization model.
BFLA (Broken Function Level Authorization) is the related failure: users accessing admin endpoints because developers assumed obscurity would prevent discovery. Attackers enumerate API paths with wordlists. Framework-generated endpoints like /actuator, /debug, and /admin are tried on every single target. Security through obscurity. Also known as no security.
Defense-in-Depth Across Layers
BOLA is the headline, but it’s far from the only threat. Effective API security stacks controls so each layer catches what the others miss. Fence, door, room locks, cameras, guards.
| Layer | Controls | What It Catches | Bypass Means |
|---|---|---|---|
| Edge / WAF | DDoS protection, IP reputation, geo-blocking | Volumetric attacks, known malicious IPs, bot traffic | Attacker uses clean residential IP |
| API Gateway | Token validation (JWT/OAuth), rate limiting, schema validation (OpenAPI) | Invalid tokens, quota abuse, malformed requests | Valid token, well-formed but malicious payload |
| Service Mesh | Mutual TLS (mTLS), service identity verification | Service impersonation, man-in-the-middle between services | Compromised service with valid certificate |
| Application | Object-level authorization (BOLA check), function-level authorization | IDOR attacks, privilege escalation, unauthorized data access | Nothing (this is the last line of defense) |
Skip a layer and you leave a gap attackers will find.
Schema validation at the gateway enforces your OpenAPI spec on every inbound request. Catches mass assignment attacks before they reach application code. If the spec says POST /users accepts name and email, the gateway rejects any request that includes role, admin, or other undocumented fields. Takes 30 minutes to enable on most API gateways. Thirty minutes to block an entire class of privilege escalation attacks. Best ROI in your security budget.
Rate limiting slows enumeration attacks. An attacker iterating invoice IDs at 10,000 requests per second downloads your entire customer database before anyone notices. Throttle to 10 per second and your anomaly detection has time to surface the pattern. Rate limits don’t prevent BOLA. Only proper authorization does that. But they shrink the window for mass data exposure from minutes to hours. Speed bumps don’t replace door locks. They buy time for the guards.
Anomaly detection on API access patterns catches BOLA exploitation even when authorization controls are incomplete. A single user suddenly requesting 500 unique invoice IDs in an hour is an obvious signal. Build alerting on per-user unique resource ID access rates, not just total traffic volume. Total metrics look completely normal while one user quietly downloads everything.
| Layer | Control | Catches | Misses |
|---|---|---|---|
| Edge/WAF | DDoS, IP reputation, geo-blocking | Volumetric attacks, known-bad IPs | Anything with a valid token |
| Gateway | JWT validation, rate limiting, schema validation | Expired tokens, mass assignment, brute force | Business logic flaws (BOLA) |
| Application | Object-level authorization, ownership checks | BOLA, privilege escalation | New endpoints not yet in policy |
| Monitoring | Anomaly detection on access patterns | Enumeration, data exfiltration | Slow, low-volume attacks |
Investing in application security starts with getting schema validation, rate limiting, and anomaly detection in place before pen tests. Pen testers finding BOLA through unthrottled enumeration is the expected outcome without these controls.
JWT and OAuth Implementation Failures
Token-based authentication is standard. JWT implementations consistently introduce critical vulnerabilities that standard security reviews miss.
The algorithm confusion attack is the one you need to understand cold. JWTs include an alg header declaring the signing algorithm. A server configured for RS256 (asymmetric) can be exploited by an attacker who crafts a token with alg: HS256. If the server uses the RS256 public key as the HMAC secret for the symmetric HS256 verification, it accepts attacker-crafted tokens signed with the public key. The public key. The one anyone can download from your JWKS endpoint. Handing out the master key and calling it public information. Because it was.
The fix: hard-code the expected algorithm in your JWT verification library. Never accept the algorithm from the token header.
# JWT validation - hard-code algorithm, validate all claims
import jwt
def validate_token(token: str, public_key: str) -> dict:
return jwt.decode(
token,
public_key,
algorithms=["RS256"], # NEVER accept from token header
audience="api.yourapp.com", # Reject tokens for other apps
issuer="https://auth.yourapp.com", # Reject dev/staging tokens
options={
"require": ["exp", "iss", "aud", "sub"],
"verify_exp": True, # Some libraries skip this by default
}
)
Not validating the exp claim is equally common and easier to overlook. Some older JWT libraries don’t enforce token expiry by default. Don’t assume your library handles this. Check the docs. Write a test that verifies an expired token gets rejected. Trust but verify. Mostly verify.
For OAuth flows: validate the iss (issuer) claim to make sure the token came from your expected identity provider. Validate the aud (audience) claim to confirm the token was issued for your app. A valid token from your dev environment’s identity provider should never be accepted by production. This exact misconfiguration shows up in production at organizations that otherwise take security seriously. The dev key opening the production door.
- JWT verification library hard-codes the expected algorithm (RS256 or ES256, never
none) - Token expiry (
exp) is validated on every request, not assumed - Issuer (
iss) and audience (aud) claims are validated against expected values - Tokens are stored in httpOnly cookies, not localStorage (XSS grabs localStorage tokens)
- JWKS endpoint is cached with a TTL, not fetched on every validation
What the Industry Gets Wrong About API Security
“WAFs protect APIs.” WAFs catch injection attacks and known payload patterns. They can’t see BOLA, mass assignment, or business logic abuse because these attacks use valid tokens, valid request structures, and valid HTTP methods. The request looks legitimate at every layer the WAF inspects. Perfect badge. Wrong room. The fence doesn’t help.
“API scanning catches vulnerabilities.” Scanners test against known patterns. BOLA needs business context no scanner has. An endpoint returning someone else’s invoice looks identical to one returning your own at the HTTP level. Only a test that knows which invoices belong to which users catches the flaw. The scanner checks if the lock works. Doesn’t check who has the key.
“Rate limiting prevents data exfiltration.” Rate limiting slows it down. An attacker throttled to 10 requests per second still downloads your entire user database. Just takes hours instead of minutes. Rate limiting buys time for anomaly detection. It is not authorization.
WHERE account_id = :user_account_id is the single most effective API security control, and it’s the one most consistently missing. If your ORM doesn’t enforce tenant scoping by default, your authorization is opt-in. Opt-in authorization is no authorization. Opt-in seatbelts would have the same result.API Inventory and Shadow APIs
The gap between official API documentation and what’s actually running in production is consistently wider than teams expect. Prototype endpoints from six months ago. Legacy versions nobody sunset. Internal tools exposed through the same gateway. Doors nobody put on the floor plan. All unlocked.
Passive traffic analysis against your OpenAPI spec reveals them. Collect gateway traffic logs over a 2-4 week window, diff against documented endpoints, and every undocumented path that received traffic is a shadow API needing immediate security review.
Sunset headers with a hard deadline, then 410 Gone after it passes. Every active API version is attack surface that needs patching and monitoring. A 12-18 month sunset window, enforced regardless of consumer migration status, keeps the inventory manageable. API integration engineering
covers designing APIs that are secure by default from the start.
| When to sunset | When to keep |
|---|---|
| Version replaced with breaking changes | Active consumers still migrating (within sunset window) |
| Fewer than 5 active consumers remaining | Regulatory requirement for backward compatibility |
| No traffic for 30+ days | Third-party integrations with contractual SLA |
| Known security vulnerabilities unfixable in old version | Version has unique functionality not in successor |
That account enumeration from the opening. 500,000 records in three hours. Now add authorization at the data layer. Schema validation blocking unexpected fields. Rate limiting throttling the iteration. Anomaly detection flagging the pattern. Fence, door, room locks, cameras, guards. No single control stops every attack. Layered together, the 12-line Python script hits a wall at every floor.