The Cloud Database With the Door Left Open
Managed databases and buckets leak data when rules check that a caller is logged in but never that they own the record. Here is the pattern and the fix.
What this is
An exposed cloud database is a managed data store - Firebase, Firestore, an S3 or GCS bucket - whose access rules let a caller read data they have no right to. The store is not misconfigured in some exotic way. In almost every case the rules check one thing correctly and skip the thing that matters: they confirm the caller is authenticated, and never confirm the caller owns the record. Authentication is checked. Authorization is not.
That single missing check is the difference between a store that serves one user their own data and a store that serves any user everyone’s data. It is one of the most common critical findings we report, and it rarely looks like a bug in the code. It looks like a rule that reads as reasonable until you ask it the second question.
Why it keeps happening
Managed data stores moved the security boundary out of the application and into a rules file, and the rules file is easy to get half right. A developer writes a rule that says “only logged-in users can read this collection,” ships it, and moves on. It feels like a lock. It is a lock on the building’s front door with no lock on any of the individual apartments inside.
The pressure that produces this is ordinary. A team wiring up Firebase or a bucket wants the app to work, and the fastest rule that makes the app work while keeping out anonymous strangers is request.auth != null. It passes review because reviewers read it as “you must be authenticated,” which is true, and stop there. The question that catches it - “authenticated as whom, and does that person own this row?” - is exactly the question that gets skipped when the goal is to ship the feature.
The same shape appears in storage buckets. A bucket starts public because that was convenient during development, or directory listing gets left on because someone needed to browse files once, and the temporary state becomes the shipped state. And there is a subtler trap: a bucket can be gated by two independent systems - cloud IAM and the storage service’s own rules - and a team that locks one can reasonably believe the data is protected while the other stands wide open over the same bytes.
How the attack works
The mechanism is the same across every variant, and it is unglamorous:
- Find the store. The app’s own client code tells you where its data lives - a Firebase project ID, a bucket name, an API host. Black-box, from the browser, this is visible.
- Register or connect. Where the rule only checks auth-presence, the attacker signs up for a normal account through the app’s own front door. Where the bucket is public or listing is on, even that is unnecessary.
- Read past your own row. With a valid session, the attacker queries the collection without the filter the app’s UI would normally apply, or lists the bucket root, or reads a sibling document by ID. The rule sees an authenticated caller, returns
200, and hands back data belonging to other tenants.
There is no exploit chain here, no memory corruption, no clever payload. The store does exactly what its rules permit. The attacker simply asks for more than the UI ever asks for, and the rules say yes.
What it looks like in the wild
We have found this across sectors that have nothing in common except the technology underneath, which is what makes it a pattern.
-
A healthcare care-navigation startup ran a Firestore collection that any client could read without authentication. It held roughly 130 mental-health patient records - diagnoses, clinical questionnaire scores - and, in the same store, stored multi-factor authentication codes. The rule never scoped reads to the patient; the collection was simply open. Full teardown: The Mental-Health Records Anyone Could Read.
-
A healthcare voice-AI company protected its case collection with a rule that only checked whether the caller was signed in. We registered an ordinary account through the product’s own signup, and from that account read every patient’s case data, not just our own. The lock was real; it just opened for everyone who had a key, and anyone could get a key. Full teardown: Sign Up, Then Read Every Patient.
-
A contract-AI SaaS had done the harder work well: its cloud IAM layer was locked down. But the Firebase Storage rules over the same bucket were wide open, and a normal storage-level check sailed straight past the IAM gate to reach thousands of customer legal documents. Two layers over one bucket, one of them forgotten. Full teardown: The Helper Proxy That Owned the Cloud.
-
A furnished-housing platform left directory listing enabled on the document store behind its app. No account was needed - the store simply enumerated its own contents, and browsing that index surfaced thousands of tenant identity documents and lease agreements. Full teardown: Directory Listing on the Document Store.
-
An EV-charging platform left a cloud bucket public that was never meant to be. Inside it: application source code, credentials, and customer personal data - the kind of mixed bag that turns one open bucket into the starting point for a much larger compromise.
The consistent theme: the data was never behind a broken lock. It was behind a lock that checked the wrong thing, or no lock the team remembered was there.
How to tell if you are exposed
A short self-check any team can run:
- Read your own rules for the phrase
request.auth != null(or its equivalent). Every place it appears without a comparison to the record’s owner or tenant is a place where any authenticated user reads everyone’s data. - From an account you registered yourself, query a collection without the filter your UI applies. If you get back rows that belong to other users, the store is authorizing on login, not ownership.
- List your buckets and check two things separately: is the bucket public, and is directory listing on? Then check the storage-service rules and the cloud IAM policy independently - a lock on one is not a lock on the other.
- Point a public-exposure scan at your own project IDs and bucket names. If an anonymous client can read anything, assume someone already has.
How to fix it
- Authorize on ownership, not on authentication. Every read and write rule must compare the caller’s identity to the record’s owner or tenant -
request.auth.uid == resource.data.ownerId, or the tenant equivalent. “Is the caller logged in” is never a sufficient rule for data that belongs to someone. - Default to private. Buckets and collections start closed. Public is a deliberate, reviewed exception with a documented reason, not a default anyone can leave in place.
- Disable directory listing. A store should never enumerate its own contents to an untrusted caller. Turn listing off and serve objects only by explicit, authorized reference.
- Check both layers over a bucket. Cloud IAM and the storage service’s own rules are two separate gates over the same data. Audit them independently and assume neither one covers for the other.
- Scan your public surface on a schedule. Project IDs and bucket names are discoverable. Test them from an unauthenticated client the way an attacker would, routinely, not once at launch.
The takeaway
Authentication asks who you are; authorization asks what you are allowed to touch. An exposed cloud database is almost always a store that answered the first question and skipped the second. If your rules confirm the caller is logged in but not that the caller owns the record, every account is a master key. This teardown pattern is covered in depth across The Mental-Health Records Anyone Could Read and its siblings.
Frequently asked
What is an exposed cloud database?
Why is checking request.auth != null not enough?
How do you secure a Firebase or S3 bucket?
This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.
How it works →



