On 25 July 2025, someone posted on 4chan that a dating safety app called Tea was serving its user verification uploads out of a storage bucket with no authentication on it. Not a weak password. Not a leaked key that can be rotated. No authentication.
If you knew the URL, you could read it.
The uploads were selfies and photos of government-issued IDs. Women had sent them to the app specifically to prove they were real people, because the whole promise of the product was that it was a safe space for women. About 72,000 images were exposed. Roughly 13,000 of them were the selfies and ID photos.
Three days later a second dataset turned up: 1.1 million private messages.
Nobody broke in. That is the part I want to talk about.
The detail everyone skipped
Most of the coverage framed this as a company being careless. I think that reading is too comfortable, because it lets the rest of us off the hook.
Here is what Tea actually said. The exposed data lived in a legacy storage system. They had already migrated to better infrastructure in February 2024. The old bucket was simply still there, still holding the most sensitive data the company had ever collected, still readable by anyone.
So the migration happened. Somebody did the right work. They stood up the new thing and moved onto it. What nobody did was go back and turn the old thing off.
That is not carelessness. That is a common oversight in Software Development. Every engineer reading this has left something running that they meant to come back to.
The fuse on that one burned for about eighteen months.
Most product I have worked on started as a POC
You open the bucket so the upload demo works in the standup. You tell yourself you will lock it down before launch, and you mean it. Then the POC gets a logo, a domain, a pricing page, and users. Nobody ever goes back, because going back is not a ticket and nothing is on fire.
The shortcut does not stay in the prototype. It ships with everything else, quietly, as part of the system.
And the thing about storage buckets specifically is that they are invisible from the inside. Your app works. Your tests pass. Uploads upload. There is nothing in your codebase and nothing on your dashboard that says this is world readable.
The only place that fact is visible is from the outside, from a client holding no credentials at all.
That is a nasty property. It means the bug cannot be caught by any of the habits we have built. Code review does not catch it. Unit tests do not catch it. You have to go stand outside your own house and try the door.
Now do that in the agentic era
I use agentic coding daily. It is genuinely great and I am not about to argue against it. But it changes the arithmetic here, and we should be honest about how.
Generating a working prototype used to take a week. It now takes an afternoon. The bottleneck moved. What did not move is the amount of review attention any of us have. So the ratio of code that exists to code that a human has actually
looked at is climbing, and it is climbing fastest in exactly the category of work that gets deferred: infrastructure, permissions, storage, the boring plumbing around the interesting feature.
Ask an agent to add file uploads and you will get file uploads. It will work on the first try. Whether the bucket policy it generated is public depends on defaults and phrasing, and you will not notice either way, because the feature works.
That is the whole problem in one sentence. The default that gets you to a demo is the same default that leaks in production, and both of them look like success.
We used to be slow enough that the gap between "it works" and "someone checked it" stayed small. We are not slow anymore.
So I built the outside-the-house check
I wanted something with no excuse not to run. Not a platform. Not an account. Not a five-step onboarding. One command, on any bucket, before it ever gets a domain.
npx bucket-audit my-bucket
It is called bucket-audit.
It looks at your bucket the way an anonymous stranger does. No AWS credentials, no SDK, no dependencies at all. It runs on Node built-ins, so npx starts it instantly.
It checks the things a stranger would try. Can I list the bucket, seven different ways. Can I read an object. Can I write or delete one, if you opt in. Is plain HTTP served. Is CORS wide open. Are the sixteen config sub-resources readable, things like ?acl, ?policy, ?publicAccessBlock. Are there eighty-odd predictable files sitting there, your .env, your .git/config, a backup.sql.
Then it scores it and tells you what to fix.
Here it is against one of our own buckets:
That bucket is not a demo. It is trac-hav-uploads, which is where the uploads from TRAC, our farm app, actually live. TRAC registers smallholder farmers and tracks what they deliver, so what sits in there belongs to real people who handed it to us on the assumption we would look after it.
I ran it in public because I would rather find out in eight seconds than read about it on 4chan. It scored 100. If it had not, I would have fixed it and then still written this article, because the useful version of this argument is not "look how secure we are," it is "here is the check, go run it."
The important part of that output is not the grade. It is that it took eight seconds and cost nothing, so there is no version of "we'll do it later" that survives contact with it.
It also fails loudly on purpose, so you can put it in CI and stop relying on anyone remembering:
npx bucket-audit my-bucket --fail-on high
That exits non-zero on anything high or critical. Thirty seconds of pipeline config, and the check now runs forever without a human in the loop. Which is the only kind of security control that actually survives a busy quarter.
Anything that needs real AWS credentials is deliberately out of scope. Block Public Access flags, the actual policy JSON, all of that lives in the console and the CLI and you should check it there. This tool answers exactly one question: what can a stranger see right now.
What I would actually do this week
Not a maturity model. Four things, in order of how much they would have helped Tea.
Go find what you already left running. Not the bucket you are building today.
The one from the migration two years ago. The one from the POC that became the product. If it still holds data, either secure it or delete it. "We moved off it" is not the same as "it is off."
Point the audit at everything before it gets a domain. The cheapest moment to fix a public bucket is before anyone has copied anything out of it. There is no second cheapest moment. Once it is enumerable, scraping it is a short script, and fixing the permissions afterwards does not un-copy a single file.
Treat verification data as radioactive. There is a real difference between a leaked password and a leaked driver's licence. One of them you rotate. The other one is that person's face and their legal identity, and there is no reissuing it.
If you collect ID documents, that bucket deserves a different level of paranoia than your avatars, and probably a deletion policy measured in days.
Assume the agent's defaults are demo defaults. When an agent generates storage, a policy, or a CORS rule, read that part. Not all of it. That part. It is the one category where "it works" tells you nothing about whether it is safe.
Security is everyone's job, and mostly nobody's
We say the first half out loud a lot. The second half is the true part. Security is everyone's job right up until there is a deadline, and then it is the job of whoever has slack, and nobody has slack.
The only fix I have found that actually holds is to make the check cheap enough that it does not need a champion. Not a policy. Not a quarterly review. A command that takes eight seconds and a CI line that fails the build.
Tea did the hard part. They migrated. They built the better thing. They just never went back and checked the door on the old one, and 13,000 women paid for that with photographs of their faces and their legal identity documents.
Go check your buckets. Especially the old ones.
npx bucket-audit my-bucket --fail-on high
Fix your tea.
Who we are
I build at Hibgli Agro Limited, HAV. We are an AgriTech company in Nigeria working in palm oil, across 1,000+ plantation plots and a growing network of smallholder farmers. The job is to connect the people who actually grow the crop directly to the buyers who want it, so that more of the money ends up with the farmer instead of a chain of middlemen.
Technology is the only reason that works at any scale. Sourcing, logistics, pricing, distribution, farmer records, all of it runs on software, and TRAC is where a lot of that lives. Technology enables us. AI multiplies us. A team our size now ships things that would have needed a department five years ago.
Which is exactly why I wrote this. The same leverage that lets a small team move like a big one also lets a small team leave a bucket open like a big one. You do not get the first half without earning the second.
If you want to see what we are building, hibgli.com and the blog, where we write about the supply chain, the farmers, and the tech behind it.
bucket-audit is MIT licensed and lives at github.com/HAV-Engineering/bucket-audit.
Only run it against buckets you own or are authorised to test.
Sources:
Security.org breach summary ·
National Law Review, on the legacy storage system ·
ABA, technical and legal analysis of the misconfiguration ·
TechReport, on the second dataset

Top comments (0)