Home/Journal/Teardown
TeardownCriticaldefault-credentialsself-hosteddocument-security

The Document Vault Behind a Default Password

A legal-AI platform left the vendor default password on its self-hosted document engine, exposing millions of live case files. Here is the chain and the fix.

Rahul Dharan··9 min read·a legal-AI platform for law firmsDefault credentials on an internet-facing service (CWE-1392/798)

The setup

The target was a legal-AI platform built for personal-injury law firms. Its job is to ingest the documents a case is made of - medical records, demand letters, settlement paperwork, correspondence - and turn that pile into something a firm can search, summarize, and act on. That means the platform is, at its core, a document vault. The most sensitive material a plaintiff ever hands a lawyer passes through it and comes to rest inside it.

That is the reason it was worth a careful look. When the entire value of a product is holding other people’s confidential files, the storage layer is the thing an attacker cares about, and it is the thing that has to be right. We went in black-box, from the open internet, with no account and no relationship to the company.

Recon

Working from the outside, we enumerated the platform’s internet-facing hosts and looked at what each one was actually running. Most of the surface was unremarkable. One host stood out: it was running a commercial self-hosted document engine - an off-the-shelf component whose job is to store and render documents at scale. It is a real product that many teams deploy behind their own applications, which is exactly why it is interesting. Off-the-shelf components come with off-the-shelf defaults.

We noticed something else. There were two instances of the same engine reachable: one host labelled as if it were a development or staging box, and one that was clearly the production instance. Two twins running the same software.

The hypothesis

Self-hosted engines have to boot on first install, so they ship with documented default credentials - an admin username and password printed in the vendor’s own getting-started guide. Changing those defaults is a manual step the deploying team is expected to remember. Under deadline pressure, on a host that “is only dev,” it is an easy step to skip.

So the hypothesis was direct: at least one of these two instances would still accept the engine’s out-of-the-box defaults. And because this particular engine’s own documentation describes an administrative API token that grants full access to everything it stores, a default that still worked would not be a cosmetic finding. It would be the whole vault.

What we tried

We started at the admin dashboard of the dev-labelled host and entered the engine’s documented default username and password - the literal pair from the vendor’s setup guide, nothing guessed or brute-forced. It logged us in.

A dashboard login is a foothold, not yet the data. The engine’s real power is its API, protected by a separate administrative token. Here is the detail that turns a login into a breach: that token is set by a different configuration variable than the dashboard password, and per the engine’s documentation it too has a factory default. We tried the literal default token against the API. It was accepted. Per the engine’s own docs, that token grants full access to all stored data.

At that point we deliberately slowed down. We had a token that the documentation said could download, extract, create, copy, and delete any document in the store. We did not exercise those. To prove access without touching client data, we made one metadata read against a single real case file - enough to confirm the token was live and the file was real, and nothing more. The file’s metadata described a genuine multi-thousand-page case bundle. We read that one record’s metadata and stopped.

The other half of the test was the control. We took the same two defaults - the dashboard pair and the API token - to the production twin. Both were rejected. Production had been hardened correctly.

What we found

The dev-labelled host was not a dev host in any meaningful sense. It was serving live production data.

The engine’s index, readable with the default token, reported millions of stored case documents. More telling than the count was the motion: timestamps in the index were updating while we were making our requests. New documents were landing on this host in real time. Whatever label was on the box, production traffic was writing to it.

The access the default token granted was total across all three properties that matter:

-> GET /<engine-api>/documents/<id>/metadata
   Authorization: Token <vendor-default-token>

<- 200 OK
   {
     "id": "<id>",
     "pages": <several thousand>,
     "created": "<recent>",
     "modified": "<updating live>",
     ... real case-file metadata ...
   }

No foothold, no credentials of our own, no exploit code. Two published defaults, entered in order, and the vault was open.

Why it happens

Nobody here was careless in the way that word usually implies. This is a specific, common trap, and it is worth naming precisely.

A self-hosted component ships with default secrets so it can start at all. Hardening it means changing those secrets - and the important detail is that they are not one secret. The dashboard password and the administrative API token are set by separate configuration variables. A team can change the one they see in the login screen and never realize a second default is still live on the API underneath. Hardening feels done when it is half done.

Layer on two more ordinary things. First, the team clearly did harden production - the twin rejected both defaults - which is exactly what a competent team does. But hardening production tells you nothing about staging; each host is configured independently, and the safe one is no evidence about the exposed one. Second, a “dev” label created a false sense of low stakes on a host that was quietly taking live production writes. The label described an intention, not the reality of the data flowing through it.

None of this is exotic. It is what shipping fast looks like when a component has more than one default and more than one home.

For developers

If you run a self-hosted component that holds real data, these controls close this class:

  1. Change every default secret, and verify each one independently. Do not stop at the password on the login screen. Enumerate every credential the component ships with - dashboard logins, API tokens, service accounts - and confirm each is changed by testing the old value and watching it fail. A default you did not know existed is the one that gets used.
  2. Map each secret to its configuration variable. When secrets are set by separate variables, treat them as separate tasks with separate verification. Changing one variable is not evidence about another.
  3. Never trust a “dev” or “staging” label to mean low-value data. If a host can receive production writes, it is a production host and must be hardened like one. Verify what data actually flows to it, not what its name implies.
  4. Keep any host holding real data off the public internet. A document engine serving an internal application has no reason to be internet-reachable. Bind it to the private network and treat public exposure as an alert.
  5. Inventory your self-hosted components against their default-credential documentation. For each one, keep a list of the defaults the vendor ships and a check that each has been rotated. The vendor’s manual is the attacker’s manual.
  6. Do not reason from one host to its twin. The fact that production is hardened is not evidence that staging is. Test each host on its own.

The takeaway

The strongest defense on one host tells you nothing about the host next to it. This platform hardened its production document engine correctly and left an identically powerful twin open, because the two secrets that lock it are set by two different variables and a “dev” label hid the fact that live case files were landing there. Default credentials are not a quaint problem - on a component whose job is to hold the most sensitive documents a person owns, one published password and one published token were the entire distance between the internet and the vault.

This teardown is one instance of a pattern we see repeatedly: a data store reachable by anyone who knows where to look. We wrote up the general case, and how to defend against it, in The Cloud Database With the Door Left Open.

Frequently asked

What is a default credentials vulnerability?
It is when an internet-facing service still accepts the username, password, or token that its software shipped with out of the box. Because those defaults are published in the vendor's own documentation, anyone can look them up and log in. The service is effectively unauthenticated to anyone who reads the manual.
Why are self-hosted components a common source of default-credential exposure?
A self-hosted component ships with default secrets so it can start on first boot. Hardening it is a manual step the deploying team has to remember, and those secrets are often set by separate configuration variables. Changing one and forgetting another leaves a door open, and a component that is hardened in production tells you nothing about its twin in staging.
How do you prevent default-credential exposure on self-hosted software?
Change every default secret before the service is reachable, verify each one independently, and never trust that hardening one host or one variable covers the rest. Inventory each self-hosted component against its vendor default-credential documentation, and keep any host holding real data off the public internet regardless of how it is labelled.

This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.

How it works →