Nobody breached Stripe. Somebody just asked the whole internet for its .env file, and hundreds of merchants handed one over.
On August 18 an actor posted an archive of live Stripe merchant secret keys and 688,363 customer records. Stripe was not compromised. Hudson Rock's analysis of the dump found no infostealer infections tied to the victim domains and no shared plugin or framework, and their working hypothesis is the dullest one available: bots requesting GET /.env from every site on the internet. That reframes the fix. Secret scanning on GitHub does not see a file your own web server is serving. Here is the sixty-second check, what an sk_live key actually lets someone do, the three Stripe controls almost no small merchant has switched on, and how to read your own key's request log to find out whether anyone else has been using it. Updated September 7 with the other route to the same outcome: a server compromise, as in the unpatched Magento zero-day now being exploited, hands over every key in your environment file at once. Updated September 8 with a third route that the sixty-second check cannot catch, because your server was never asked for the file: a poisoned dependency read the environment from the inside. Updated September 14 with a fourth: an unauthenticated file read in GitLab, and the finding that its packaging writes the keys that decrypt the whole instance at mode 0644, which is the one-line permissions check worth running on your own stack today. Updated September 16 with a fifth route that none of the checks here would catch, because it is not your server: a mass-scanning campaign is pulling .env files out of exposed Vite development servers, which is the machine your website was built on rather than the one it runs on.
There is a specific kind of security incident that produces headlines a business owner cannot act on. Something with a name and a logo gets breached, the coverage is about the vendor, and the only available response is to feel worried and wait.
This is the other kind. Nothing you buy from was breached. The credentials that leaked were sitting on servers the victims own, in a file their own web server was willing to hand to anyone who asked for it by name. Every step of the fix is inside your control, and the first step takes about sixty seconds.
What was posted
On August 18, 2026, an actor using the alias “Satanic” posted an archive on a data trading forum. It was published for free download rather than sold. The contents were live Stripe merchant API keys and the customer and payment data pulled through them.
Counts differ depending on who did the counting, which is worth being upfront about because you will see both numbers.
| Figure | Source |
|---|---|
659 merchant accounts, roughly 650 live sk_live keys |
Ransomnews |
1,033 live sk_live keys, 669 advertised vendor folders, 323 uniquely identifiable business domains |
Hudson Rock |
| 688,363 customer records across 17,654 files, 42 countries | Reported consistently across both |
| Archive advertised at 33 to 35 GB; actual download 2.37 GB | Hudson Rock |
| Roughly 20,000 further keys held back for staggered release | Actor’s claim, not verified |
Hudson Rock is explicit that the 20,000 figure “is an actor claim to Hudson Rock researchers, not an independent count,” and we would treat it the same way. Take the verified part seriously and the boast as a boast.
The geography skews the way internet exposure usually does: 212 affected merchants in the United States, 81 in the United Kingdom, 57 in France. The records themselves run from January 2022 to June 2026, which tells you these were not fresh compromises. They were long-standing exposures that somebody finally got around to collecting.
The contents are the uncomfortable part. Customer names, email addresses, phone numbers, home addresses, IP addresses. Charges, payment intents, invoices, refunds, disputes, payouts, balance transactions. Partial card data, meaning last four digits, from rendered invoices. Promotional codes. On the largest victims, transaction records covering more than 22,000 customers and over five million dollars in paid invoices. And per one analysis, 519 of the accounts had both payment and payout capability enabled.
Stripe’s infrastructure was not touched. Every advisory we read agrees on this, and the Black Swan advisory of August 19 puts it plainly: the evidence “suggests this is not a compromise of Stripe infrastructure itself, but rather the compromise and collection of merchant-owned API credentials.” Ransomnews says it analysed the files offline and notified Stripe before publishing. We could not find a public statement from Stripe about the dump, and we are not going to invent one.
The part that changes what you should do about it
The first wave of coverage assumed infostealer malware. That is the standard explanation for a pile of credentials appearing at once: someone’s laptop got infected, the malware scraped saved secrets, the logs got traded.
Hudson Rock checked, and it does not hold. They found no infostealer infections associated with the specific vendor domains in the dump. They also found no common thread in the victims’ technology: PHP storefronts, Node services, Python APIs and hosted site builders, with no shared plugin, framework or dependency that would explain a single exploited vulnerability.
What they did find, in origin access logs from the weeks before the dump appeared, were requests. GET /.env. GET /.git/HEAD. From scanner user agents. Their working hypothesis is automated mass-scanning for publicly exposed environment files and debug logs.
If that is right, and the evidence lines up better with it than with anything else, then this was not an attack on anybody. Nobody chose these businesses. A bot walked the internet asking every host for a file at a predictable path, and a few hundred servers said yes.
That distinction matters more than it sounds like it should, because “we are too small to be a target” is the single most common reason small businesses skip this work, and it is not wrong so much as irrelevant. You were never a target. You were a row in a scan.
The sixty-second check
Open a terminal and ask your own site for the file:
curl -sI https://yourdomain.com/.env
A 404 or 403 in the first line is what you want. A 200 is an emergency, and so is anything that returns file contents. Do the same for a few other paths that get scanned constantly:
curl -sI https://yourdomain.com/.git/HEAD
curl -sI https://yourdomain.com/.env.local
curl -sI https://yourdomain.com/.env.backup
curl -sI https://yourdomain.com/config.json
curl -sI https://yourdomain.com/storage/logs/laravel.log
Check every hostname, not just the main one. Staging subdomains, the old site still sitting at old.yourdomain.com, the client preview environment nobody turned off, the WordPress install at /blog that predates the current site. Exposure lives in the forgotten hosts far more often than the main one.
This is not a rare misconfiguration. Mysterium VPN published research on February 27, 2026 that identified 12,088,677 IP addresses serving publicly accessible .env-style files, with roughly 2.8 million of them in the United States alone. Separately, Palo Alto’s Unit 42 documented an extortion operation built entirely on scanning for exposed environment files, which found credentials across more than 110,000 domains. And Intruder’s 2026 Attack Surface Management Index, drawn from 3,000 attack surfaces, found 30 percent had “files or information publicly accessible that shouldn’t be,” config files among them.
Why does it happen? Almost always the same handful of causes. The document root got pointed at the project directory instead of public/. A deploy copied the whole repository to the server. Someone added .env.example to the repo, then a colleague renamed a working copy in place. A server migration lost the Nginx or Apache rule that blocked dotfiles. None of these are exotic mistakes. They are the kind of thing that happens once during a rushed launch and then sits there for four years.
What a leaked sk_live key actually does
Worth being precise here, because the answer is worse than most people assume and the reason is structural.
Stripe issues three kinds of key. A publishable key, prefixed pk_, is designed to sit in your front-end code and is safe to expose. A restricted key, prefixed rk_, does only what you gave it permission to do. A secret key, prefixed sk_, has, in Stripe’s own words, “unrestricted permissions on all Stripe APIs.”
Stripe’s documentation is unusually direct about this: “Because you can’t limit their permissions, we don’t recommend using secret keys for new use cases, and for existing integrations, we recommend migrating secret key usage to RAKs.”
An unrestricted live secret key lets whoever holds it list your customers and their stored payment methods, create charges and payment links, issue refunds, read your full transaction history, and modify your webhook endpoints. That last one is quieter than the others and deserves a mention on its own: an attacker who repoints a webhook is not stealing money today, they are intercepting the notifications that would tell you money is moving.
Ransomnews illustrated the speed. After finding an active key, researchers reached the merchant’s customer list, created a fraudulent payment link and put through a one dollar test charge inside 17 hours.
One documented constraint is worth knowing, and worth not overstating. Stripe’s documentation says an API key “might have its access limited” if it has not been used to create transfers or payouts, or to update payout destinations, for more than 180 days. A limited key cannot create payouts or transfers or add payout destinations until someone clicks Restore access in the Dashboard. For a typical small merchant whose integration only ever creates charges, that is a real speed bump between a leaked key and money leaving the account. It is not protection. It does nothing about customer data, charges, refunds or webhooks, and it is a default you should verify rather than rely on.
Why GitHub’s secret scanning did not save these merchants
There is a good safety net for leaked Stripe keys and it covers the wrong place.
GitHub’s secret scanning runs automatically and free on public repositories. Stripe participates in the partner program, so when GitHub matches a live Stripe key pattern in a public repo it notifies Stripe directly, and Stripe can revoke the key. Push protection goes further and blocks the commit before it lands. Both are genuinely good, and if you are not using push protection on your repositories, turn it on this week.
Now look at the exposure paths in this incident: a .env file served over HTTP by your own web server, a debug log in a public directory, a .git directory left in the document root, an unmasked value in a build log, a container image layer, a database backup in a public bucket.
GitHub sees none of that. Secret scanning covers content on GitHub, and private and internal repositories need GitHub Secret Protection enabled before it applies to them at all. A file your own Nginx is happily serving to the internet is outside the system entirely.
This is the general shape of the problem and it is not specific to Stripe. We wrote about the npm supply chain attacks and about self-hosted AI tools ending up on CISA’s actively exploited list, and the same structure keeps showing up: a well-built control that covers one distribution channel, and a credential that leaks through a different one. The safety net is real, and the hole in it is exactly the shape of a small business deployment that happened quickly.
Three Stripe controls almost nobody has switched on
Stripe has shipped good tooling for this. It is off by default because it has to be, and most small merchants have never opened the page.
Restricted API keys. A restricted key is a drop-in replacement for a secret key in your server environment. Same code, different value. The difference is that you set each resource to None, Read or Write, so a key that only creates payments can only create payments. Stripe’s own framing: “If a bad actor obtains a RAK, they’re limited to that key’s permissions.”
The migration path is straightforward and does not require guessing at permissions. Open the API keys page in the Dashboard, use the overflow menu next to your existing secret key and select View request logs. That shows you every call your integration actually makes. GET requests are reads, POST and DELETE are writes. Build the key from that list, test it in a sandbox, and Stripe will tell you in the error body which permission is missing if you got it wrong. Then rotate out the secret key.
Note for anyone wiring an AI agent into their payment stack, which is a live topic right now: Stripe recommends restricted keys “especially when giving a key to an AI agent,” with permissions used to bound what the agent can do. That is the correct instinct, and it sits alongside everything we wrote about what prompt injection means for a business running AI agents.
Access policies. This one is underused and it is the strongest control on the list. You can attach a policy to a live key that restricts where it can be used from: specific IP addresses or CIDR ranges, or an advanced policy by autonomous system number and country, with the option to block anonymous VPNs, public proxies, residential proxies and Tor exit nodes. Requests that fail the policy are blocked and Stripe notifies you.
Read that last clause again. A leaked key with an access policy on it does not just fail when someone in another country tries it. It tells you it happened. Stripe recommends configuring access policies on all live mode keys, and if your application runs on fixed IPs or inside one cloud provider’s ASN, this is maybe twenty minutes of work.
Rotation with the grace period. The reason people do not rotate keys is the fear of taking the site down at the wrong moment. Stripe removed that excuse. Rotating a key in the Dashboard keeps both old and new working for up to seven days, so you can deploy the new one gradually, watch the old key’s request logs, and expire it once its volume has sat at zero. If you need longer than seven days, create a new key manually, migrate, then expire the old one.
If you have concrete reason to think a key is exposed, skip all of that and rotate with expiry set to Now, which deletes the old key immediately. Brief downtime beats an open key.
How to find out if someone else has been using your key
Rotating is the fix. This is the diagnosis, and most people never run it.
Every Stripe API key has its own request log. API keys page, overflow menu next to the key, View request logs, which opens Workbench. What you are looking for is traffic that does not match your application: calls at hours your site is asleep, endpoints your code never touches, GET /v1/customers pulls with pagination when your app only ever fetches one customer at a time, refunds you cannot tie to a support ticket.
Alongside that, four things to reconcile in the Dashboard:
- Webhook endpoints. Every URL listed should be one you recognise. An extra endpoint is not a misconfiguration, it is an intercept.
- Payout destinations and payout history. Confirm the bank account on file is yours and that recent payouts went where you expect.
- Refunds over the last several months. Cross-check against your own records. Refunds to attacker-controlled destinations are a common monetisation path and they look mundane in a summary view.
- Team members and connected apps. Anything with standing access to the account, not just keys.
If you find unauthorised activity, the order is: rotate immediately with expiry Now, then contact Stripe support, then work out how the key got out, in that order. Do not spend the first hour on forensics while the key is still live.
The obligation is yours, and Stripe wrote that down
On August 14, 2026, four days before the archive appeared, Stripe published a user notice on API key security. We have no evidence the two are connected, and a legal notice of that kind is normally drafted weeks ahead, so read this as context rather than foreshadowing.
The relevant line: “you are responsible for maintaining the security of your API keys and ensuring they are used only by authorized parties.” The notice sets out what Stripe expects, which is roughly the list above. Store keys securely, use restricted keys rather than broad secret keys, apply IP allowlisting and expiry, rotate, monitor for unauthorised access and respond promptly.
It is easy to read that as a company distancing itself from liability, and it partly is. It is also just true. There is no version of this where the payment processor can protect a credential that your own server is publishing to the internet.
The checklist
If you take payments online, this is an hour, and most of it is checking rather than changing.
- Run the sixty-second check against every hostname you own, production and otherwise.
curl -sI https://host/.envand the.git/HEADvariant, on the main site, staging, old subdomains and any legacy install. - If anything returned a 200, treat every credential in that file as public. Not just Stripe. Database passwords, SMTP credentials, JWT signing secrets, cloud tokens, third-party API keys. Rotate all of them.
- Rotate your Stripe live secret key using the seven-day grace period if there is no evidence of compromise, or immediately if there is.
- Replace secret keys with restricted keys, one per service. Build the permission list from the request logs, not from guesswork.
- Attach an access policy to every live key. IP ranges if you have fixed IPs, ASN and country if you are on a cloud provider. The alerting is worth as much as the blocking.
- Read the request log for each live key and reconcile webhooks, payout destinations, refunds and team access.
- Turn on push protection for your repositories, and confirm whether your private repos have secret scanning at all.
- Fix the server config, not just the file. Block dotfiles at the web server, point the document root at the public directory, and confirm your deploy process is not copying the repository onto the server. Deleting one
.envand leaving the rule missing means the next one is exposed too. - Move keys out of files entirely where your platform allows it. A secrets vault or the hosting platform’s environment variable store is not exotic any more, and it is what Stripe’s own best practice guidance asks for.
What this is actually about
We build and maintain payment integrations for Toronto small businesses, and the honest version of why exposures like this persist is not negligence. It is that the person who deployed the site is not the person who runs the business, and often is not around any more.
Every one of the 323 identifiable businesses in this dump had a working site, taking real payments, with a file sitting at a predictable path that nobody had looked at since launch. There was no alert for it because nothing was broken. The site worked. Payments cleared. The exposure produced no symptom at all until an archive appeared on a forum.
That is the category of risk worth building a habit around, and the habit is small: a short list of everything you own that answers on port 443, and one person who runs a handful of checks against it every quarter. It does not need tooling or a budget. It needs someone to be responsible for it.
If you are not sure who that is for your business, or you ran the check at the top of this article and got a 200, get in touch. The first part of that conversation is short.
Update, September 7, 2026: the other way your key leaves the building. This post is about a key being served to the internet by your own web server. There is a second route to the same outcome and it surfaced this week, so it belongs here.
On September 4, attackers began exploiting an unpatched remote code execution chain in Magento and Adobe Commerce that Sansec named StyleSmuggler. Successful exploitation gives code execution as the web user, and the forensics from the hosting provider Disrex found the implant reading the store’s session storage over Redis. Anything readable by that user is gone, and on a self-hosted store that includes app/etc/env.php, your environment file, and every live secret key in it.
The relevant point for this article is that the response is the same either way. A compromised server means every credential on it is compromised, whether or not you can see it being used. Rotate with the Now expiry option rather than the seven-day grace period, because the grace period is a window the attacker also gets. Then read the per-key request logs described above and look for calls you cannot account for, which is the only evidence available that someone else was using the key before you rotated it.
If you run a self-hosted store, the full incident write-up including indicators of compromise is here, and you should check for those before you rotate anything, because rotating a key on a box that still has an implant on it just gives the attacker the new one.
Update, September 8, 2026: and a third route, which needs no exploit at all. The two routes above both start with something reaching your server from outside. The third starts with your server reaching out and pulling something in.
On March 24, 2026, two malicious releases of LiteLLM, a widely used open-source AI proxy, were published to PyPI and stayed up for roughly forty minutes. One of them shipped a file that Python runs automatically on interpreter startup, and its job was to read the process environment, encrypt it and send it to an attacker-controlled domain. Cloud keys, SSH keys, Kubernetes tokens, database passwords, and model API keys, whatever the process could see.
That is the same theft as this article describes, with a different first step. It is worth adding here for one reason: nothing in the sixty-second check further up this post would have caught it. Your .env file was never served to anyone. It was read from the inside by code you asked for. The defences that do apply are the ones about the keys themselves: per-application rather than shared, spend-capped, and rotated on a schedule you set rather than on the day you find out. We wrote the full account of that incident in the supply chain post, and what else the affected component holds in the AI gateway post.
Update, September 14, 2026: a fourth route, and the reason the sixty-second check is not enough on its own.
The three routes above are: the misconfigured server that hands out .env to anyone who asks, the compromised server that gets read from the inside, and the poisoned dependency that reads the environment on startup. Here is a fourth, and it is the one that most undermines the instinct that “my secrets file is not in the web root, so I am fine. Updated September 20, 2026 with the copy of the key you forgot you made: rotation fixes production and does nothing to git history, which matters more now that coding agents have twice been caught uploading whole .git directories.”
On September 10 GitLab patched CVE-2026-85706, a CVSS 10.0 flaw that lets an unauthenticated stranger read arbitrary files off the server. CISA confirmed active exploitation the next day. A file read is not a shell, and on most systems that is a meaningful distinction, because the interesting files are locked down.
We went and checked whether that is true on a GitLab box, and it is not. GitLab’s Omnibus packaging writes /etc/gitlab/gitlab-secrets.json at mode 0600, root owned, which is correct. It then writes a second copy of the same encryption keys to /var/opt/gitlab/gitlab-rails/etc/secrets.yml at mode 0644, world readable, because the application needs to read them. We verified both in the packaging source at the 19.3.2 release tag. The good permissions on the first file buy nothing, because the second file opens the same locks.
The generalisable lesson is not about GitLab. It is that the file permissions on your secrets are set by your framework’s packaging, not by your intentions, and almost nobody has looked. If a bug in your stack ever yields a single arbitrary file read, the question that decides whether it is an incident or a footnote is what mode your application’s key material sits at on disk. That is a one-line check you can run today:
ls -l config/secrets.yml .env* 2>/dev/null
Anything ending in rw-r--r-- is readable by every process on that host. It should be rw-------, owned by the account the application runs as. The full account of the GitLab flaw, including what those particular keys unlock, is here.
Update, September 16, 2026: a fifth route, and it is not your server at all.
Four routes so far, and every one of them runs through a machine you would call production: the web server that hands out .env, the compromised server read from the inside, the poisoned dependency, the file-read bug in a self-hosted tool. The fifth starts somewhere nobody puts on an inventory. It starts on the laptop where the site was built.
F5 Labs published telemetry on September 11 showing a scanning campaign that ran more than 32,000 requests in August against Vite development servers, up from a baseline of 1,732 across the previous three months, pulling .env files, AWS credentials and Terraform state out of any that answered. Vite is the build tool underneath Astro, Nuxt, SvelteKit, Remix, Storybook and the Laravel front-end pipeline, so it is on the machine of nearly everybody who builds websites for a living. The flaw, CVE-2026-39364, lets an unauthenticated request retrieve the .env the dev server is specifically configured to refuse, by appending ?raw?? to it.
Two things make this worth adding to a post about Stripe keys.
The first is that none of the checks in this article would have caught it. Your web server was never asked for the file. Your dependencies were not poisoned. Your permissions were correct. A process that was only ever supposed to listen on localhost was started with --host so somebody could test the site on their phone, or was put in a Docker container where binding to all interfaces is the standard fix, and for as long as it ran it would answer that request from anyone who could reach the port.
The second is what a developer’s .env usually holds compared with a server’s. It is often the same secret key, because test and live keys get swapped in the same file, and it frequently holds more of them: the mail provider, the database, the analytics account, the deploy token. We wrote this one up in full, including a finding from our own testing that matters here: the patched releases on Vite 6, 7 and 8.0 close the .env hole and still serve .npmrc, which is where the npm publish token lives.
The practical addition to the checklist at the top of this post: when you rotate, rotate for the developer machines too, not only for the server. And if anyone on the project has ever run a dev server with --host on shared wifi, that is a date to check your Stripe key’s request log against.
Update, September 20, 2026: the copy of the key you forgot you made
One addition, because it is the single most common gap we find after someone has done everything on the checklist above.
Rotating a key fixes the live system. It does nothing to your version control history. If that key was ever committed, even once, even in a file deleted the same afternoon, the old value is still sitting in the .git directory on every machine that has ever cloned the repository. Deleting a file in git is a commit that adds a record of the deletion; it does not remove the object.
That matters more this month than usual. In September a desktop AI coding agent was found packaging entire workspaces, with .git making up the large majority of the payload by volume, and uploading them to vendor cloud storage without disclosure. One affected company’s account of what went up included database passwords and cloud service credentials. None of those were in the working tree. They were in the history.
Two commands, worth running once on every repository you own:
git log -p --all -S 'sk_live_' --oneline
git log -p --all -S 'BEGIN PRIVATE KEY' --oneline
For a real audit use gitleaks detect --no-git=false or trufflehog git file://., which walk the full object graph rather than the current checkout. If something turns up, rotation is still the fix, and it is now urgent rather than housekeeping. The longer piece on what coding agents send, and where, is here.
Sources
- Analyzing Stripe Vendors Breach: Confirmed Vendor Exposure and Claims of 20,000 Compromised APIs, Hudson Rock, for the August 18 posting date, the actor alias and forum, the 1,033 live
sk_livekeys across 669 advertised folders and 323 uniquely identifiable domains, the 2.37 GB actual download against a 33 GB claim, the absence of infostealer infections tied to the vendor domains, the heterogeneous technology stacks, theGET /.envandGET /.git/HEADscanner traffic in origin access logs, and the explicit caveat that the 20,000 figure is an actor claim - Live API keys expose 688,363 Stripe customer records across 42 countries, CybersecAsia, for the 659 merchant accounts, 650 live secret keys, 17,654 files, the 42 countries and per-country breakdown, the January 2022 to June 2026 date range, and the 519 accounts with both payment and payout capability
- 50,000 Stripe Secrets Leaked in Public Code, Security Affairs, for the Ransomnews research, the 50,000-plus unique keys found across public repositories, GitHub Actions logs and misconfigured servers, and the 17-hour customer list to fraudulent payment link to test charge finding
- Threat Advisory: Stripe Merchant API Keys Exposed, Black Swan Cybersecurity, August 19, 2026, for the quoted assessment that this is not a compromise of Stripe infrastructure, and for the remediation ordering
- User Notice: API Key Security, Stripe, August 14, 2026, for the merchant responsibility language and Stripe’s stated expectations on key storage, restricted keys, allowlisting, rotation and monitoring
- API keys, Stripe documentation, for the key type table and prefixes, the “unrestricted permissions on all Stripe APIs” description of secret keys, the recommendation against secret keys for new use cases, the rotation grace period of up to seven days, the Now expiry option, the per-key request logs, the access policy types including ASN, country and threat sources, and the 180-day payout access limitation and Restore access step
- Restricted API keys, Stripe documentation, for the None/Read/Write permission model, the drop-in replacement guidance, the request-log method for deriving permissions, the GET/POST/DELETE to read/write mapping, the one-key-per-service recommendation, and the recommendation to use restricted keys especially when giving a key to an AI agent
- Secret scanning, GitHub documentation, for free automatic scanning on public repositories, the partner program notification and revocation flow, and the requirement for GitHub Secret Protection on private and internal repositories
- 12 million exposed .env files reveal widespread security failures, Security Affairs, February 27, 2026, for the Mysterium VPN figure of 12,088,677 IP addresses serving publicly accessible environment files and the per-country distribution
- Leaked Environment Variables Allow Large-Scale Extortion Operation in Cloud Environments, Unit 42, Palo Alto Networks, for the extortion campaign built on scanning for exposed environment files across more than 110,000 domains
- The Top 10 Attack Surface Exposures in 2026, The Hacker News, for Intruder’s 2026 Attack Surface Management Index across 3,000 attack surfaces and the finding that 30 percent had files or information publicly accessible that should not have been