What a leaked Stripe key can do: permission by permission
Secret vs restricted vs publishable, live vs test — the exact capability differences from Stripe's own docs, plus the rotation path that limits downtime.
What a leaked Stripe key can do: permission by permission
For anyone deciding how urgently to respond to an exposed Stripe key: a capability-by-capability breakdown of each key type, drawn from Stripe's documentation, and the documented rotation procedure.
Not all Stripe keys are alike, and the difference is the whole risk assessment. A publishable key in your bundle is architecture working as intended. A secret key in your bundle is unrestricted programmatic control over a live payments account. Between those poles sit restricted keys, whose granular permissions turn "everything" into "exactly what this integration needed." Knowing precisely what each type authorizes turns a vague dread into a concrete checklist — and tells you which findings justify dropping everything.
All formats, types, and procedures below follow Stripe's keys documentation and related official pages, cited inline. Every example key in this article is a synthetic placeholder in Stripe's format; it authenticates against nothing.
The four key types
Stripe issues three account-level types plus organization keys (Key types):
| Type | Prefix | Safe to expose | What it authorizes |
|---|---|---|---|
| Publishable | pk_live_ / pk_test_ | Yes — designed for front-end code | Identifies your account for tokenization (card elements, mobile SDKs); cannot read data or retrieve sensitive information |
| Secret | sk_live_ / sk_test_ | No | Unrestricted access to all Stripe APIs for the account |
| Restricted (RAK) | rk_live_ / rk_test_ | No | Only the specific resource permissions you grant |
| Organization | sk_org_… | No | Like restricted/secret keys, but operating across accounts at the organization level |
Mode matters as much as type: test-prefixed keys operate on sandbox data isolated from live objects, so a leaked sk_test_51SYNTHETICPLACEHOLDERdoNOTship000000 exposes simulated customers and charges — worth cleaning up, categorically different from live. Live keys touch real money movement and real customer records. Stripe additionally supports managed keys issued through hosting platforms, delivered to your app by the host and rotated by it — those never pass through your hands and are outside this article's incident scope.
One adjacent credential completes the inventory: webhook signing secrets (whsec_…). These aren't API keys — they authenticate incoming webhooks to your receiver. Leaked ones enable forged-looking deliveries to you, not calls to Stripe; they're rotated per endpoint in the Dashboard's Webhooks section (Webhooks).
What a secret key authorizes, concretely
An unrestricted sk_live_ key is, functionally, your Stripe account available as an API. Enumerating capabilities from the API reference, the blast radius includes:
| Capability | Consequence of unauthorized use |
|---|---|
| Charges & PaymentIntents (create, capture) | Fraudulent transactions processed on your account, with your descriptor |
| Refunds | Money returned to attacker-chosen instruments, drained from balance |
| Balance reads | Full visibility into funds, payouts, and transaction volume |
| Payout management | Payouts redirected to attacker-controlled bank accounts |
| Customers list & retrieve | Export of stored PII: names, emails, addresses, phone numbers, metadata |
| Payment methods & saved cards | Access to stored payment-method metadata (never raw card numbers, which PCI keeps out of Stripe's API surface entirely) |
| Connect operations | Control over connected accounts where Connect is enabled |
| Webhook endpoints (create) | Persistence: attacker receives copies of future account events at their endpoint |
| Disputes & files | Manipulation of evidence flows; retrieval of uploaded documents |
Two rows deserve emphasis because they shape response priorities. Customer PII exposure triggers different obligations than financial exposure — data disclosed cannot be un-disclosed even after rotation, which is why usage-log review (below) matters as much as revocation speed. Webhook-endpoint creation is the documented persistence trick: even after you notice odd charges, an unnoticed extra endpoint keeps streaming events until you audit endpoints specifically.
None of this requires sophistication beyond reading Stripe's API docs — that is precisely the point of treating a leaked secret key as a maximum-severity incident. The defense Stripe itself recommends: don't use secret keys for new integrations at all (Best practices).
Restricted keys: permission by permission
Restricted API keys invert the model. You create a RAK, then toggle Read/Write per resource within permission groups — Core (charges, customers, payment intents…), Billing, Checkout, Connect, Reporting, and others (Restricted API keys). A RAK grants only what you checked.
Representative permissions (illustrative subset; the authoritative list is the Dashboard's permission matrix):
| Resource | Read grants | Write grants |
|---|---|---|
| Charges | View existing charges | Create/capture/refund charges |
| Customers | View customer records | Create/edit/delete customers |
| PaymentIntents | View intents | Create/confirm/cancel intents |
| Payouts | View payout history | Create payouts, edit destinations |
| Balance | View balances | — |
| Webhook endpoints | List endpoints | Create/edit/delete endpoints |
The security consequence is direct: a leaked RAK's blast radius equals exactly its toggles. A key granted only Charges: Write for a checkout integration cannot list customers, cannot redirect payouts, cannot read balances. This is why Stripe's guidance recommends RAKs for nearly all server-side use and describes migrating existing secret-key integrations to them — the migration is the single highest-leverage change most Stripe integrations can make against this entire threat class.
Scoping by integration type makes the toggles concrete:
| Integration | Sensible minimal grant |
|---|---|
| Checkout / payment links | Checkout: Write; PaymentIntents: Write |
| Subscription billing service | Billing: Read+Write; Customers: Write (no PII listing beyond need) |
| Reporting dashboard | Reporting: Read; Balance: Read — nothing writable |
| Webhook receiver administration | Webhook resources: Read+Write, isolated to its own key |
Migration ordering keeps each swap downtime-free: create the scoped key alongside the secret key, repoint one integration at a time while the old key still works (the overlap pattern), verify per-key request logs show clean cutover, then retire the broad credential. Each migration shrinks standing blast radius immediately — and because RAKs rotate independently, future rotations become integration-sized maintenance instead of account-wide events.
Publishable keys complete the picture: they exist to be public, identify your account during tokenization, and are documented as safe to expose in front-end code (Switching to live mode). Finding pk_live_ in a bundle is normal. Detection tooling that flags it is miscalibrated — the distinction between expected-public and never-public formats is central to the scanner rules.
How leaked-key abuse actually presents
Without walking through attack steps (out of scope here — see the editorial ground rules against enabling abuse), the observable aftermath patterns matter for detection and response:
- Request logs show foreign traffic. Every API call is logged and attributable per key; Stripe's request logs are the primary forensic surface (View request logs).
- Money movement anomalies: unexpected refunds, payout destination changes, unusual charge patterns.
- Data-access anomalies: bulk customer listing pagination patterns unlike your app's usage.
- Persistence artifacts: additional webhook endpoints, additional API keys, altered account settings.
Access policies add a tripwire layer Stripe recommends on all live keys: restrict a key to your servers' IP ranges or ASN, and unauthorized use from elsewhere is blocked and reported (Access policies). For a team with fixed infrastructure IPs, this converts silent misuse into active alerting.
The rotation path, from the docs
Stripe's documented rotation procedure is unusually well-suited to zero downtime (Rotate an API key):
- Rotate creates the replacement immediately. In Dashboard → Developers → API keys, choose ⋯ → Rotate key on the compromised key. The replacement works right away; the old key keeps working during a grace period of up to 7 days while you migrate consumers gradually.
- Migrate consumers deliberately. Update server environments one deployment at a time, watching error rates — Stripe suggests rolling the new key out to a subset of servers first.
- Verify silence before expiry. Before the old key expires, open its request logs and confirm traffic has been at zero long enough to catch stragglers (scheduled jobs are the usual holdouts).
- Expire. Choose Now at rotation time to delete the old key immediately — the right choice for confirmed compromise — or let the scheduled expiration close it.
If the key is actively being abused, skip the grace period: rotate with immediate expiration, accepting brief downtime as the cheaper loss. If you need longer overlap than 7 days, the documented path is creating a new key manually, migrating, then expiring the old one when its logs go quiet.
For RAKs the same flow applies per key, which is another argument for RAK adoption: rotation becomes routine maintenance scoped to one integration instead of an account-wide event. Webhook signing secrets rotate separately, per endpoint. And the full playbook — generalized dual-key overlap across providers — lives in the rotation playbook.
Prevention mapped to this surface
- Prefer RAKs everywhere; reserve secret keys for the rare full-account automation, and store those in platform secret stores (best practices).
- Attach access policies to live keys so misuse announces itself.
- Keep
pk_live_the only Stripe credential with a legitimate path into client bundles — and verify with artifact scanning, since bundlers will happily inline whatever a module references. Monitoring on deploys catches regressions; the free scan answers the question once.
Webhook signing secrets: the adjacent credential
The whsec_… secret deserves its own treatment because it inverts the direction of trust — and inversion changes the entire response calculus. API keys authenticate your calls to Stripe; webhook signing secrets authenticate Stripe's calls to you. Your endpoint verifies each delivery's signature against the secret; verification passing means the event genuinely originated from Stripe and arrived unmodified.
A disclosed signing secret therefore enables forged inbound events: fabricated invoice.paid, payment_intent.succeeded, or checkout.session.completed payloads that your fulfillment logic accepts as genuine. The concrete consequences follow your automation — order fulfillment without payment, entitlements granted, state machines advanced. Nothing touches Stripe itself; the attack lives entirely in what your code does when told payment occurred.
Response specifics differ from API-key rotation in two ways:
- Rotate per endpoint, from Dashboard → Developers → Webhooks → the endpoint's signing secret. During migration, verify signatures against both old and new secrets so in-flight deliveries don't fail — then drop the old one once rotation propagates (webhooks docs).
- Audit fulfillment paths, not Stripe logs: which endpoints accept webhooks, what each grants on success, and whether any verification gap exists (the classic one being signature checks that read parsed/re-serialized bodies instead of the raw payload — verification requires the exact bytes Stripe signed).
Detection coverage should include the whsec_ format wherever secret scanning runs; its omission from "key inventories" is common because it isn't an API key, which is exactly the misconception that lets it leak quietly (why it's flagged as high severity).
Access policies as tripwires
Stripe's access policies convert silent key misuse into blocked-and-reported attempts — a property closer to intrusion detection than to prevention, and cheap to configure:
- IP allowlists fit fixed-infrastructure servers: attach a policy permitting your egress ranges; requests from anywhere else fail authentication while generating notifications.
- Advanced policies combine allowed ASNs (your cloud provider's — AWS 16509-style identifiers), allowed countries, and blocked source categories (anonymous VPNs, public proxies, residential proxies, Tor exits). Dynamically scaling infrastructure fits here where static IPs don't.
- Separate policies per environment distinguish staging from production keys, so one environment's compromise can't impersonate another's traffic pattern.
The tripwire value survives even imperfect configuration: an attacker routing through unexpected infrastructure trips the policy regardless of what they attempt next, collapsing discovery latency from "invoice review" timescales to immediate notification. For teams running restricted keys everywhere, policies complete the containment model — scoped permissions bound what a leaked key can do; access policies surface that it's being misused while it still matters.
Live versus test: severity without guesswork
Mode prefixes make triage mechanical because the isolation is structural, not behavioral:
| Found string | Mode | Data reachable | Response posture |
|---|---|---|---|
sk_live_ / rk_live_ | Live | Real money, real customers | Immediate — runbook |
sk_test_ / rk_test_ | Sandbox | Simulated objects only | Scheduled rotation + source fix |
pk_live_ / pk_test_ | Either | Nothing readable — tokenization identity | Expected in client code; no action |
whsec_… | Per-endpoint | Your receiver's trust | Rotate; audit fulfillment paths |
The nuance worth internalizing: sandbox data isn't empty. Teams seed test environments with realistic fixtures — names, emails, sometimes copied production shapes — so a leaked test key discloses a portrait of your data model even while touching no real customer. That's why the table says "scheduled rotation" rather than "ignore": the credential dies on cadence, the leak path closes, and nobody loses sleep.
One promotion hazard deserves its own sentence: code paths drift across modes. A test key hardcoded during development has a way of surviving into the branch where someone swaps environment configuration for a demo — modes are configured, not enforced by code shape. Detection that distinguishes live from test formats (as the rules reference encodes) catches both, routing each at its true severity instead of a single alarm level.
Local webhook testing closes the verification loop during development: Stripe's CLI forwards signed events to local endpoints (stripe listen --forward-to localhost:3000/webhooks), letting receivers prove their signature verification against genuine signatures before production ever sees them. Receivers tested this way fail closed under key mismatch — exactly the property that makes whsec rotation safe during the overlap window described above.
FAQ
I found my pk_live_ key published somewhere. Is that an incident? No. Publishable keys are designed for public placement and cannot read account data. Verify it's genuinely a pk_ key (not sk_ misread), and move on.
A test-mode sk_test_ key leaked. How bad? Sandbox-isolated: no live money or customer data reachable. Rotate it to preserve hygiene — sandbox data often mirrors production schemas closely enough to be sensitive — but it doesn't outrank your other work.
Someone used our leaked secret key before we noticed. Can refunds be reversed? Work with Stripe support promptly; outcomes depend on specifics and timing. The general lesson stands: PII read via the Customers API is unrecoverable by any rotation, which is why log review determines notification obligations independently of money movement.
Do restricted keys expire? Not automatically — they persist until expired or deleted, though unused keys can face access limitations after long dormancy per Stripe's policies. Treat expiration as your discipline, not the platform's.
Knowing what each key can do is half the defense; knowing whether your deployed bundle contains one is the other. Run the free scan at keydrift.dev/scan — no signup — and check what your site actually serves.