The admin panel you left unlocked

Why a public Strapi or Directus admin is a data leak waiting to happen

You picked a headless CMS so the content team could edit without bothering you. Strapi, maybe Directus. You pointed your frontend at its API, shipped it, and the admin panel has been quietly running at /admin ever since.

Here's the thing you may not have thought about: that admin panel is the front door to all your content and users, and it's almost certainly reachable from the public internet. The frontend needs the API. The API and the admin usually live on the same host. So the panel that creates users, edits every record, and exposes API tokens is sitting one URL away from anyone who looks.

The admin and the API are not the same surface

This trips up a lot of people, so it's worth being precise.

Your public site talks to the CMS's content API: the read endpoints that serve published articles or products. That traffic is fine and expected. The admin panel is a different thing entirely. Strapi serves it at /admin, Directus at /admin too. It's a full single-page app for managing the data model, the users, the roles, the API tokens, and every record in every collection. The default install puts it on the same domain and port as the API, so if your API is public, your admin login is public by default. You didn't expose it on purpose. The framework's out-of-the-box layout did, and the quickstart docs you followed didn't stop to mention it.

cms.yoursite.com/admin
Strapiv4.15.0
Email
Password
Strapi 4.15.0·public registration check needed
Admin login renders on the public domain

What's behind that login

A CMS admin is, by design, total control over the content layer. That's the whole point of it. Which is exactly why it being public is a problem.

Once an attacker is in, whether through credential stuffing, a weak password, or a registration foothold, they own your content and your data model. They can read and export every collection, including user collections with emails and password hashes. They can mint API tokens with full permissions and use them to script the rest from a clean connection that survives a password reset. They can edit or delete published content, inject malicious markup into fields your frontend renders unescaped, and create themselves a permanent admin account. On a misconfigured Strapi, the public role can be left with read or even write permissions on collections you assumed were private, so some of this needs no login at all, just a request to the right API path.

  • Export user collections: emails, hashes, profile data.
  • Issue full-access API tokens for persistent, scriptable access.
  • Inject content into fields the frontend renders, turning the CMS into a stored XSS delivery system.
  • Read collections the public role was accidentally granted access to, no login required.
request
GET /api/users?populate=* HTTP/1.1 Host: cms.yoursite.com
response
HTTP/1.1 200 OK
Content-Type: application/json
[{
"id": 1, "email": "[email protected]",
"resetPasswordToken": "a3f9...", "role": "admin"
}, { ...every user in the table... }]
A public role left with read access to the users collection returns the whole table to an unauthenticated GET.

How it gets discovered

The same way the Grafana and Kibana panels do: the subdomain names itself. cms.yoursite.com, admin.yoursite.com, api.yoursite.com, all published in certificate transparency logs the moment you issued a cert. Scanners then request /admin and recognize the Strapi or Directus login app by its markup and asset paths. The version is fingerprintable from those same assets, and Strapi in particular has had registration and permission CVEs worth matching a version against. The first admin-registration endpoint, /admin/register-admin, is also a classic target: on some setups it stays open and lets a stranger create the very first superadmin if you never created yours.

How to lock the panel down

The content API can be public. The admin panel should not be. Separate the two.

The strongest move is to put /admin behind an IP allowlist or a VPN at the reverse proxy, so the login app simply doesn't render for an address you don't recognize, while the content API stays open on its own paths. Then audit your roles: set the public role to read-only on exactly the collections your frontend needs and nothing else, and confirm authenticated and admin permissions aren't leaking private collections. Rotate any API tokens that have been around since launch, and make sure /admin/register-admin is closed because an admin already exists.

# nginx: everything on one public host
location / {
proxy_pass http://127.0.0.1:1337;   # API and /admin both public
}
The content API stays open; the admin app only renders for allowlisted addresses.

Whether the front door renders for the public is the first question, and you can answer it in under a minute. Load /admin on your CMS domain and see whether the login app appears, then hit a private API path unauthenticated and see whether it answers. SurfaceCheckr does exactly that pass for you: it requests the panel, reads the Strapi or Directus markup and asset paths to confirm it rendered, pulls the version fingerprint, and probes /admin/register-admin to see if the first-superadmin endpoint is still open. All of it from the outside, the way a stranger hits your domain, with no login and no payloads. It can't see your role permissions from the inside, and it isn't a pentest, but it does tell you whether the panel a stranger finds is sitting there in the open.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.