DEV Community

Dark Master
Dark Master

Posted on

I scanned GitLab's source code and found 2,449 undocumented API routes

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
Enter fullscreen mode Exit fullscreen mode

For GitLab, the scanner:

  1. Parsed every routes.rb and config/routes/*.rb file
  2. Expanded every resources :projects call into its 7 REST routes
  3. Followed every concern :reviewable mounted across multiple resources
  4. Added Grape API routes from lib/api/*.rb — 1,017 routes from that alone
  5. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
CRITICAL  POST   /api/v1/internal/reset      (no auth documented)
HIGH      GET    /api/v2/users/export        (no rate limit documented)
INFO      GET    /health                     (intentionally undocumented)
Enter fullscreen mode Exit fullscreen mode

Reverse mode — find dead spec entries (spec not in code):

shadowaudit --dir . --spec openapi.yaml --reverse
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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%)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)