Last week I pointed a static analysis tool I built at GitLab's public source code. It found 2,449 routes in the actual Ruby code that don't exist anywhere in the official API documentation.
Then I ran it on Mastodon. 601 undocumented routes.
These aren't bugs — they're shadow APIs. Routes that exist, respond to requests, but were never documented. No auth requirements listed. No rate limits specified. No deprecation warnings. Just... there.
What is a shadow API?
A shadow API is any route your server handles that isn't in your OpenAPI/Swagger spec. They appear for a few reasons:
- A developer added an internal endpoint and forgot to document it
- A route was deprecated but never removed from code
- A feature was half-built — routes exist but docs were never written
- A framework auto-generates routes (Rails resources, NestJS decorators) and nobody audited what got generated
The problem: your security team can't protect what they don't know exists. Your API gateway can't rate-limit it. Your consumers can't predict when it'll break. And attackers can find it with a directory scan.
How I found 2,449 of them in GitLab
I built a CLI called shadowaudit. It does static analysis — reads your actual framework code, extracts every route, then diffs it against your OpenAPI spec.
npx shadowaudit --dir ./gitlab --spec ./openapi.yaml --framework rails
For GitLab, the scanner:
- Parsed every
routes.rbandconfig/routes/*.rbfile - Expanded every
resources :projectscall into its 7 REST routes - Followed every
concern :reviewablemounted across multiple resources - Added Grape API routes from
lib/api/*.rb— 1,017 routes from that alone - Diffed all detected routes against the public GitLab API spec
Result: 2,449 routes in code with no match in the spec.
Some are intentional internals. Some are legacy. Some are genuinely undocumented public endpoints that clients are probably hitting right now.
The tool: shadowaudit
npm install -g shadowaudit
shadowaudit --dir ./your-api --spec ./openapi.yaml
7 frameworks supported:
- Express (including 3-level nested router chains)
- Fastify
- NestJS (including
@Version()decorators) - Koa
- Hapi
- Rails
- Grape
Three modes:
Default — find shadow routes (code not in spec):
shadowaudit --dir . --spec openapi.yaml
CRITICAL POST /api/v1/internal/reset (no auth documented)
HIGH GET /api/v2/users/export (no rate limit documented)
INFO GET /health (intentionally undocumented)
Reverse mode — find dead spec entries (spec not in code):
shadowaudit --dir . --spec openapi.yaml --reverse
Finds routes your spec documents that no longer exist in code. Dead docs that confuse your consumers.
Coverage scoring — how complete is your OpenAPI spec?
shadowaudit --dir . --spec openapi.yaml --coverage
Coverage Score: 67/100
Descriptions: 142/200 endpoints (71%)
Parameters: 89/134 params (66%)
Response codes: 67/200 endpoints (33.5%) ← missing 4xx/5xx
Security: 45/67 endpoints (67%)
CI integration — fails the build if shadow routes exist:
# .github/workflows/api-audit.yml
- name: Shadow API Audit
run: npx shadowaudit --dir . --spec openapi.yaml --format markdown >> $GITHUB_STEP_SUMMARY
Exit codes: 0 = clean, 1 = findings, 2 = tool error. CI-safe by design.
Ignore known-good routes:
shadowaudit --dir . --spec openapi.yaml --ignore-paths /health,/metrics,/ping
What makes this different from existing tools
Most shadow API detection tools work at runtime — they watch traffic and flag requests to unknown endpoints. That means:
- You need production traffic to find them
- You find out AFTER deployment
- You need an API gateway or proxy in the path
shadowaudit works at development time, on static code, with zero infrastructure. Run it in CI and catch shadow routes before they ever reach production. No proxy. No traffic. No production access needed.
The closest thing is manually diffing your routes against your spec — which nobody does because it's tedious and breaks every time someone adds a route.
Real-world results
| Target | Routes in code | Routes in spec | Shadow routes |
|---|---|---|---|
| GitLab | 2,449 | ~800 | ~1,649 |
| Mastodon | 766 | ~165 | ~601 |
These numbers aren't accusations — both projects are open source and their "shadow" routes are largely intentional internals. But the point stands: even well-maintained APIs with dedicated teams have hundreds of routes that exist outside their documented surface area.
For a smaller team shipping fast, the gap is usually worse.
Try it
# Scan your API against your spec
npx shadowaudit --dir . --spec openapi.yaml
# Add to CI (GitHub Actions)
npx shadowaudit --dir . --spec openapi.yaml --format markdown >> $GITHUB_STEP_SUMMARY
# Score your OpenAPI spec completeness
npx shadowaudit --dir . --spec openapi.yaml --coverage
# Find dead spec entries
npx shadowaudit --dir . --spec openapi.yaml --reverse
npm: shadowaudit (published under darkmaster0345)
GitHub: github.com/darkmaster0345/shadowaudit
It's free. MIT licensed. 459 tests passing across 7 frameworks.
If you find a shadow route you didn't know about — I'd genuinely like to hear about it.
Built this in 8 days as a side project. v1.0.1 shipped yesterday with hardening fixes from an adversarial review — exit code hygiene, Express deep router chain support, NestJS versioning. Feedback welcome.
Tags: security api opensource devops webdev
Top comments (0)