How shadowaudit found 269 shadow routes in Ghost CMS
We ran shadowaudit against Ghost CMS — a popular open-source Node.js blogging platform — to see how it performs on a production-grade Express codebase.
Why Ghost CMS?
Ghost is a real production app, not a toy boilerplate. It powers publications like 404Media, Platformer, and The Browser. It has:
- A large Express.js API surface (admin API, content API, members API, webhooks)
- Custom auth middleware (
mw.authAdminApi) - Complex route mounting patterns
- A security disclosure policy
If shadowaudit can handle Ghost, it can handle most Express codebases.
Setup
git clone https://github.com/TryGhost/Ghost.git --depth=1
cd Ghost
# Scan the web layer (where Express routes live)
shadowaudit --dir ghost/core/core/server/web --framework express
Raw results — v0.2.1
The first scan with shadowaudit v0.2.1 found 313 routes, but the results had two problems:
| Metric | Count | Issue |
|---|---|---|
| Total routes detected | 313 | — |
| Real HTTP routes | 302 | ✅ Accurate |
config.get() false positives | 11 | ❌ config.get('paths') matched as GET paths |
| Auth detected | 20 | ❌ Should be 235 — Ghost uses mw.authAdminApi which wasn't in the hardcoded pattern list |
| Routes reported as "no auth" | 293 | ❌ 213 of these actually HAD auth (mw.authAdminApi) |
Root causes:
- No object-name filtering — shadowaudit matched
.get()on ANY object (config.get(),settings.get()), not just Express objects (app.get(),router.get()) - String-based auth detection — shadowaudit couldn't recognize
authAdminApias auth because it wasn't in the hardcoded pattern list
After v0.3.0 — AST-based auth detection
v0.3.0 shipped two improvements that directly addressed these issues:
- Object-name filtering — only match route calls on
app,router,api,serverobjects - AST-based auth detection — walk the route's CallExpression arguments and check if any middleware name contains
auth,login,session,token,jwt,passport,permission,require,protect,secure, orverify
Same scan with v0.3.0:
| Metric | v0.2.1 | v0.3.0 | Improvement |
|---|---|---|---|
config.get() false positives | 11 | 0 | ✅ Eliminated |
| Auth detected | 20 | 235 | ✅ 12x improvement |
| False negatives (no auth) | 293 | 34 | ✅ 88% reduction |
| Total routes | 313 | 269 | More accurate |
The 34 remaining "no auth" routes are legitimately public — session endpoints (POST /session), authentication endpoints (POST /authentication/password_reset), setup endpoints (POST /authentication/setup), and webhooks (POST /webhooks/stripe). These are expected to be public.
What it caught
shadowaudit correctly identified the security-sensitive routes in Ghost's admin API:
/db— database export/import (CRITICAL if undocumented, but Ghost hasmw.authAdminApion it)/db/backup— database backup/themes/upload— theme file upload/images/upload— image upload/users/:id/token— user token generation/integrations/:id/api_key/:keyid/refresh— API key refresh
All of these had mw.authAdminApi applied — and after v0.3.0, shadowaudit correctly detected that auth.
Conclusion
This validates that shadowaudit works on real, production-grade codebases — not just toy examples. The Ghost CMS scan directly shaped the v0.3.0 roadmap:
- Object-name filtering (roadmap item #1) — eliminated
config.get()false positives - AST-based auth detection (roadmap item #2) — caught custom
authAdminApimiddleware without needing config patterns
The remaining 34 "no auth" routes were verified as legitimately public endpoints. Zero real vulnerabilities were found — Ghost's admin API is properly secured.
Want to test shadowaudit on your codebase? Get started →
