I run the API gateway in front of a few dozen services, and the endpoint that keeps me up at night is never the one I wrote a test for. It is the one nobody told me shipped. The OWASP API Security Top 10 names that exact failure (Improper Inventory Management, API9:2023), but most teams pin the list to a wall instead of working it as a backlog.
These tips are for whoever owns the gateway, the WAF, or the platform, and has to answer a CISO asking "do we even know every API we expose?" Each one is tied to a specific OWASP category, a real breach, or a number you can check. No "rotate your keys" filler.
TL;DR: The current edition is 2023, and the two categories that wreck real systems are API1 (Broken Object Level Authorization) and API9 (Improper Inventory Management). Find shadow and zombie endpoints with runtime eBPF discovery instead of trusting your OpenAPI file, replay one user request with another user token in CI, and fail the build when a live route is missing from the spec. The rest of the list gets much easier once you actually know what you expose.
Is there an OWASP API Top 10 2025?
1. Treat the 2023 edition as current and stop waiting for an "API 2025" list
There is no 2025 edition of the OWASP API Security Top 10. The "OWASP Top 10 2025" you keep seeing referenced is the general web-application list, a separate project.
The API list's newest edition is 2023, which merged Excessive Data Exposure and Mass Assignment into Broken Object Property Level Authorization (API3) and renamed Improper Assets Management to Improper Inventory Management (API9). Map your controls to the 2023 categories now, not to a draft that does not exist.
How do you find shadow and zombie APIs?
2. Find them with runtime discovery, not your OpenAPI spec
Audits routinely put 30 to 40% of an organization's real API footprint in shadow (undocumented) or zombie (deprecated but still live) endpoints, and only 15% of orgs say they trust their own inventory (AppSec Santa, 2026). Your spec file describes what you meant to ship, not what is listening.
eBPF-based discovery captures close to 100% of calls at the kernel for 1 to 3% CPU overhead and no code changes (Invicti, 2026). It is the same kernel vantage point the runtime security agents already use, so if one of those is deployed you have most of the plumbing. On Kubernetes, Pixie gives you a fast read of live HTTP routes:
px run px/http_data -o json | jq -r '.req_path' | sort -u
Diff that against your spec. Every path in the first set and not the second is a finding.
How do you test for BOLA before an attacker does?
3. Replay user A's request with user B's token
Broken Object Level Authorization is API1:2023 for a reason: it shows up in roughly 40% of all API attacks (Salt Security). It is also cheap to test. For every endpoint that takes an object id, fire the request as user A, then replay it byte-for-byte with user B's token and assert a 403 or 404.
# expect 403/404, not 200
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $USER_B_TOKEN" \
https://api.internal/v1/invoices/$USER_A_INVOICE_ID
A 200 means you have a BOLA, and a generic scanner will often miss it because the response looks valid. Put this in CI so it fails the build, not in a quarterly pentest.
How do you stop a zombie endpoint from living for years?
4. Give every endpoint a deprecation TTL
The 2022 Optus breach exposed 9.8 million customers through a public API that had been internet-facing since 2020, carrying an access-control coding error introduced as early as 2018 (2022 Optus data breach). That is a zombie endpoint that lived for four years.
Tag routes with a sunset date in gateway metadata and alert when one passes it with live traffic.
# Envoy/Gateway route metadata
metadata:
sunset: "2026-09-01"
owner: billing-team
Anything past sunset still serving traffic is a ticket, not a maybe.
Which fields and flows should the API refuse?
5. Lock down object properties at both ends to close API3:2023
Broken Object Property Level Authorization is the merge of over-exposing response fields and accepting fields you should not (mass assignment). Do not serialize your whole ORM model. Define an explicit response allowlist, and reject unknown keys on input so an attacker cannot set "role":"admin" by guessing field names.
class InvoiceOut(BaseModel):
id: str
amount: int
# no internal_notes, no owner_id
class InvoiceIn(BaseModel):
model_config = ConfigDict(extra="forbid") # reject unknown fields
amount: int
6. Rate-limit by business flow, not just per-IP, for API4 and API6
Unrestricted Resource Consumption (API4) and Unrestricted Access to Sensitive Business Flows (API6) are not stopped by a flat "100 req/min per IP." An attacker scalping concert tickets or brute-forcing OTPs spreads across hundreds of addresses.
Limit the costly flow on its business key: OTP verifications per account per hour, password resets per user per day, checkout attempts per session. The counter belongs on the identity, not the network address.
What should the gateway do with a route that has no auth policy?
7. Make it default-deny, because one missing check is the whole breach
The Optus endpoint required no authentication at all. Configure the gateway so a route with no auth policy is rejected at deploy time, not served.
In an Istio or Envoy Gateway setup, attach a RequestAuthentication plus an AuthorizationPolicy, set the cluster default to deny, and fail CI lint if any route lacks one. A new route inherits "deny" until someone explicitly opens it. If you are in the middle of a move to Gateway API, that migration is the cheapest moment to make deny the default, since every route object is being rewritten anyway.
Why does SSRF get its own category now?
8. Allowlist outbound destinations to stop SSRF (API7:2023)
Server Side Request Forgery became its own category in 2023 because so many APIs fetch user-supplied URLs (webhooks, image imports, link previews). If your service can be told to fetch http://169.254.169.254/, it is now a cloud-metadata exfiltration tool. Resolve the host and block private, link-local, and loopback ranges before the request leaves your process.
import ipaddress, socket
ip = ipaddress.ip_address(socket.gethostbyname(host))
if ip.is_private or ip.is_link_local or ip.is_loopback:
raise ValueError("blocked SSRF target")
Resolve once and connect to that resolved IP, or a DNS rebind will slip past you between the check and the fetch. A default-deny egress policy at the network layer is the backstop for the day this application-level check is bypassed.
Do you trust the APIs you consume?
9. Distrust the APIs you consume, not only the ones you expose (API10:2023)
Unsafe Consumption of APIs covers the third-party responses your code swallows without checking. A partner's compromised API can hand you a malicious redirect, an oversized payload, or injected data you pass straight to your database.
Set timeouts, cap response size, and validate the response schema the same way you validate inbound requests. A vendor's 200 is untrusted input, and the credential your service uses to call that vendor belongs in the same non-human identity inventory as everything else that authenticates without a human present.
How do you keep shadow APIs from shipping again?
10. Fail the build when a live route is missing from the spec
This operationalizes API9 (Improper Inventory Management). In CI, hit the running service's route table and diff it against the committed OpenAPI file. An undeclared route fails the pipeline.
This is the single control that turns shadow APIs from a quarterly audit surprise into a pull-request comment, and it is the one I would ship first. The full loop, from live traffic to a named owner, looks like this:
Wrap-up
If you do only one of these, do number 10: make undocumented routes fail the build. Every breach above started as an endpoint someone forgot, and you cannot defend an API you do not know is running. Here is the order I would work it, with the trigger that fires each step:
- This week, run eBPF discovery and diff against your spec (tip 2). If shadow plus zombie endpoints exceed 10% of your live routes, freeze new API merges until the gap is documented. The industry baseline is 30 to 40%, so crossing 10% means you are already losing track.
- Add the BOLA replay test to CI (tip 3). If any object-id endpoint returns 200 for another user's token, it is a release blocker, not a backlog item. BOLA is 40% of real attacks.
- Wire the spec-vs-runtime diff into the pipeline (tip 10). If even one live route is missing from the spec, fail that build the same day.
- Tag every route with a sunset date and default-deny auth (tips 4 and 7). If a route passes its sunset still serving traffic, or ships with no auth policy, page the owner. Optus is the price of a four-year-old miss: 9.8 million records.
If you would rather have the discovery pass run and the BOLA replay wired into your pipeline for you, get in touch.
FAQ
Is there an OWASP API Security Top 10 2025? No. The newest edition of the API list is 2023. The "OWASP Top 10 2025" you keep seeing referenced is the general web-application list, a separate project, so map your controls to the 2023 API categories.
What is the difference between a shadow API and a zombie API? A shadow API is live but undocumented, so it never appears in your OpenAPI file. A zombie API is deprecated but still serving traffic. Audits routinely put 30 to 40% of a real API footprint in those two buckets combined.
How do I test for BOLA in CI? For every endpoint that takes an object id, send the request as one user, then replay it byte-for-byte with a second user's token and assert 403 or 404. A 200 is a Broken Object Level Authorization finding and should fail the build.
Will a generic scanner or WAF catch BOLA? Often not, because the response to the replayed request looks like a valid 200. Detection needs an identity-aware replay that knows which object belongs to which user, which is why the check belongs in CI rather than in a quarterly pentest.
How do I stop SSRF in an API that fetches user-supplied URLs? Resolve the hostname yourself and block private, link-local, and loopback ranges before the request leaves your process, then connect to that resolved IP. Connecting by hostname after the check lets a DNS rebind slip past you.
What I read
- https://owasp.org/API-Security/editions/2023/en/0x04-release-notes/
- https://salt.security/blog/api1-2023-broken-object-level-authentication
- https://www.invicti.com/blog/web-security/api-discovery-kubernetes-runtime
- https://en.wikipedia.org/wiki/2022_Optus_data_breach
- https://appsecsanta.com/research/api-security-statistics

Comments
Be the first to comment.