API09: Improper Inventory Management - Prevention
Prevention Strategy Overview
Preventing improper inventory management is a program, not a patch. It combines knowing what you run (inventory and discovery), governing how it changes (versioning, deprecation, environment separation), and watching it continuously (documentation as source of truth, monitoring, external scanning). The defenses below are layered so that a miss at one layer is caught by another.
Core Principles
- You can't protect what you can't see: Discovery must be active and automated, not tribal knowledge.
- Deprecated means gone: A version is retired only when it returns errors, not data.
- Uniform controls: Every version and host enforces the same authN/authZ, TLS, and rate limits.
- Documentation is the source of truth: The spec drives the deployment, not the other way around.
- Continuous, not periodic: Inventory is wired into CI/CD and re-verified constantly.
1. Build and Maintain an API Inventory
The foundation is a single authoritative catalog of every API: host, version, owner, data classification, auth model, and lifecycle status. Treat it as code so it stays current.
# api-inventory.yaml -- version-controlled, reviewed on every change
apis:
- name: users-api
host: api.example.com
versions:
- version: v3
status: active
auth: oauth2 + scopes
data_classification: PII
owner: identity-team
openapi: specs/users-v3.yaml
- version: v2
status: deprecated
sunset_date: 2024-06-30
owner: identity-team
- version: v1
status: retired # MUST return 410, verified by CI
retired_date: 2023-01-15
- name: payments-api
host: payments.example.com
versions:
- version: v1
status: active
auth: mTLS
data_classification: PCI
owner: payments-team
Enrich the catalog automatically from the sources that already know about your traffic: the API gateway config, service mesh, load-balancer rules, DNS, and cloud provider inventories. Reconcile discovered endpoints against the declared inventory and flag anything unaccounted for.
2. Versioning and a Real Deprecation Policy
Adopt an explicit versioning scheme and, crucially, a lifecycle with enforced sunset dates. Announce deprecation with headers so clients migrate before the shutdown.
# Advertise deprecation and sunset on responses (RFC 8594 Sunset header)
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 30 Jun 2024 23:59:59 GMT
Link: <https://api.example.com/v3/users>; rel="successor-version"
Warning: 299 - "v2 is deprecated; migrate to v3 before the Sunset date"
When the sunset date passes, the version must return 410 Gone — and that behavior should be asserted by an automated test so a retired version can never silently come back to life.
# CI guard: retired versions MUST be gone
def test_retired_versions_return_410():
for host, path in RETIRED_ENDPOINTS:
r = requests.get(f"https://{host}{path}", timeout=5)
assert r.status_code == 410, f"{host}{path} is still live!"
3. Environment Separation
Non-production must never be reachable with production data over the public internet. Enforce separation at the network, data, and configuration layers.
- Network: Put dev/staging/QA behind a VPN, IP allowlist, or private network — never a public IP with a public DNS record.
- Data: Seed non-prod with synthetic or irreversibly masked data, never a raw copy of production.
- Config: Disable debug modes, verbose errors, and diagnostic endpoints outside development.
# Example: block public exposure of non-prod hosts at the edge (nginx)
map $host $is_nonprod {
default 0;
"~*^(dev|staging|qa|uat)-" 1;
}
server {
if ($is_nonprod) {
# only allow the corporate VPN range
allow 10.20.0.0/16;
deny all;
}
}
4. Documentation (OpenAPI) as the Source of Truth
Make a machine-readable specification the authoritative contract, generate or validate it in CI, and diff it on every change so a new or undocumented endpoint cannot ship unnoticed.
# CI step: fail the build if the running app exposes routes not in the spec
# (spec-vs-implementation drift detection)
spectral lint openapi.yaml # lint the spec itself
schemathesis run openapi.yaml --checks all # test impl against the spec
# Diff the generated spec against the committed one
oasdiff breaking committed-openapi.yaml generated-openapi.yaml \
&& echo "No undocumented drift" \
|| (echo "Undocumented endpoint or breaking change detected"; exit 1)
Do not serve internal specs publicly. Interactive docs,
openapi.json, and GraphQL introspection should be disabled or authenticated in production. The spec is a governance tool, not a public map of your attack surface.
5. Disable Framework Diagnostic Endpoints in Production
Framework defaults are a leading source of shadow exposure. Turn them off explicitly.
# Spring Boot: expose nothing by default, secure what you keep
management.endpoints.web.exposure.include=health
management.endpoints.web.exposure.exclude=env,heapdump,mappings,beans
management.endpoint.health.show-details=never
# And put /actuator behind authentication + a private network
# FastAPI / Flask: no interactive docs or introspection in prod
app = FastAPI(
docs_url=None, # disable Swagger UI
redoc_url=None, # disable ReDoc
openapi_url=None, # do not serve the spec publicly
)
6. External-Facing Asset Discovery
Attackers enumerate your surface; you must enumerate it first. Run continuous external discovery and reconcile the results against your inventory.
# Scheduled discovery pipeline (illustrative)
# 1. Pull hostnames from certificate transparency
curl -s "https://crt.sh/?q=%25.example.com&output=json" | jq -r '.[].name_value' \
| sort -u > discovered_hosts.txt
# 2. Resolve + probe which are actually live
httpx -l discovered_hosts.txt -status-code -title -tech-detect > live_hosts.txt
# 3. Diff against the declared inventory; alert on anything new
comm -23 <(sort live_hosts.txt) <(sort inventory_hosts.txt) > UNKNOWN_ASSETS.txt
[ -s UNKNOWN_ASSETS.txt ] && notify-security "Unmanaged assets found"
Complement scanning with a DNS hygiene process: remove records for retired hosts and watch for dangling records that could be subdomain-takeover targets.
7. Enforce Control Parity Across Versions and Hosts
Centralize security so that a new control automatically applies everywhere, rather than being re-implemented per version. An API gateway is the natural enforcement point — but only if all traffic routes through it.
# Gateway policy applied to every route, every version (illustrative)
policies:
- match: "/api/**" # all paths, all versions
require_auth: oauth2
require_tls: "1.2+"
rate_limit: 100/min
deny_if_unlisted: true # reject routes not in the registered inventory
The deny_if_unlisted pattern is powerful: the gateway rejects any path that is not explicitly registered, so a shadow endpoint deployed behind the gateway fails closed instead of silently serving traffic.
TLS and Access Parity
Old versions and stale hosts frequently run outdated TLS or accept credentials the current surface has rotated. Enforce a single TLS baseline and a single credential lifecycle across every host — deprecated endpoints must not be a soft spot for downgrade or replay.
8. Monitoring and Detection
Every endpoint in the inventory must also be in your telemetry. Alert specifically on signals of inventory failure.
- Traffic to deprecated/retired versions: any hit on a supposedly-retired path is both a migration signal and a potential attack.
- Requests to unregistered routes: 404s that cluster (fuzzing) or unexpected 200s on unlisted paths.
- Access to diagnostic paths:
/actuator,/swagger,/_debug,/metricsfrom outside trusted ranges. - New hosts appearing in CT logs that are not in the inventory.
def monitor_inventory_signals(request):
alerts = []
if request.path.startswith(("/api/v1", "/api/v2/legacy")):
alerts.append(f"Traffic to deprecated endpoint: {request.path}")
if request.path not in REGISTERED_ROUTES:
alerts.append(f"Request to unregistered route: {request.path}")
if any(p in request.path for p in ("/actuator", "/_debug", "/swagger", "/metrics")):
if not is_internal(request.remote_addr):
alerts.append(f"External access to diagnostic path: {request.path}")
if alerts:
log.warning("inventory_signal", extra={"alerts": alerts, "src": request.remote_addr})
send_security_alert(alerts)
9. Govern Third-Party Data Flows
Extend the inventory to cover integrations: for each partner or vendor connection, record what data is shared, in which direction, under what authentication, and who owns the relationship. Review these on the same cadence as your own endpoints, and include them in breach-impact analysis.
Prevention Checklist
- [ ] A single, version-controlled inventory of every API host and version exists
- [ ] Discovered assets are reconciled against the inventory automatically
- [ ] Every version has a documented status and enforced sunset date
- [ ] Retired versions return
410, asserted by an automated test - [ ] Non-prod is off the public internet and uses masked data
- [ ] OpenAPI spec is validated in CI and not served publicly
- [ ] Framework diagnostic endpoints are disabled/authenticated in prod
- [ ] External discovery (CT logs, subdomains, ports) runs on a schedule
- [ ] The gateway enforces uniform controls and rejects unlisted routes
- [ ] Monitoring alerts on deprecated, unregistered, and diagnostic access
- [ ] Third-party data flows are inventoried and reviewed
Key Takeaways
- Inventory-as-code — a reviewed, machine-readable catalog is the foundation.
- Deprecation needs teeth — enforce sunset with
410and CI assertions. - Separate environments hard — non-prod off the internet, data masked.
- Spec drives deployment — detect drift in CI, don't publish the spec.
- Discover before attackers do — automate external scanning.
- Enforce parity centrally — one gateway policy for all versions; fail closed on unlisted routes.
- Monitor the whole inventory — every known endpoint feeds telemetry.
Next Steps
- Code Examples: See vulnerable vs. secure implementations across frameworks
- Attack Vectors: Understand what you're defending against
- Hands-On Lab: Practice discovering and retiring unmanaged endpoints