Skip to main content

How shadowaudit found 269 shadow routes in Ghost CMS

· 3 min read
Ubaid ur Rehman
Creator of shadowaudit

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:

MetricCountIssue
Total routes detected313
Real HTTP routes302✅ Accurate
config.get() false positives11config.get('paths') matched as GET paths
Auth detected20❌ 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:

  1. No object-name filtering — shadowaudit matched .get() on ANY object (config.get(), settings.get()), not just Express objects (app.get(), router.get())
  2. String-based auth detection — shadowaudit couldn't recognize authAdminApi as 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:

  1. Object-name filtering — only match route calls on app, router, api, server objects
  2. 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, or verify

Same scan with v0.3.0:

Metricv0.2.1v0.3.0Improvement
config.get() false positives110✅ Eliminated
Auth detected20235✅ 12x improvement
False negatives (no auth)29334✅ 88% reduction
Total routes313269More 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 has mw.authAdminApi on 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 authAdminApi middleware 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 →