Full app access on incomplete payment (Knock-to-Unlock) is a Stripe integration vulnerability where an app grants full paid access to a user who never completed subscription payment.
Public disclosure: January 8, 2026
Knock: The bad actor starts a subscription checkout that asks for 3D Secure (3DS) payment authentication. They then ignore the 3DS step.
Unlock: Happens only if the application has the wrong webhook logic.
The Knock triggers Stripe's customer.subscription.created webhook event, and
the app incorrectly grants full paid access without checking subscription
status is active. The customer never completed the payment.
With the app now Unlocked, the malicious user simply abandons the checkout, navigates back to the application, and can access all the paid features.
This is one of the most severe billing vulnerabilities, because the attacker can get full access to the app without even having a card on file.
To make things worse, that free access doesn't end next month on subscription renewal. It persists indefinitely - the attacker doesn't even have to renew it.
The good news: the fix is straightforward.
Steps to reproduce
The assumption here is that your app uses Stripe and implements subscriptions. Only test apps you own or have permission to test.
- Create an account in the target application.
- Start a checkout, choose a subscription plan.
- Fill all checkout fields as required - including payment card.
- Knock: start the payment, without actually paying. Repeat until a 3DS authentication is initiated. More on this below.
- After you get 3DS modal - ignore it! Close that browser tab or leave it untouched.
- Open target application in a new browser tab.
- Unlock: if the application is vulnerable, you'll have access to paid features.
Lets zoom in on step 4: the Knock.
Test mode
If you're testing in a non-production environment (dev or staging) using Stripe
test mode, just use a test card that requires 3DS authentication.
For example: 4000 0000 0000 3220.
Things are a little trickier if you need to test in production with Stripe live mode using a real card.
Live mode
Start the payment without paying
In the Knock step you "start the payment, without actually paying". Here are two ways to achieve this:
Lock the card - most banking apps allow you to temporarily lock or freeze your card to control costs.
Limit the spending - this is another common feature of banking apps. If you set the spending limit to a very low value like $0.01, your card becomes practically disabled.
After you enable one of these options, you can use the card details in checkout without risking a real charge.
Starting 3DS auth
Getting a 3DS challenge is easy now: just retry the payment (without paying) a couple times, and the 3DS modal should show up.
Here's what you can do if 3DS challenge is not showing up:
- Check your bank supports 3DS.
- Are you using a U.S. bank? Apparently they'd much rather fail the transaction then start 3DS challenge.
- The truth is, 3DS is entirely up to the bank: its internal rules, risk scoring, machine learning, and so on. Sometimes you just won't get a 3DS prompt, and there's nothing you can do about it.
- If all fails, try another card - or another bank. Neobanks usually have good 3DS support and some aggressively trigger a 3DS auth.
Even though I've outlined the steps above, I strongly recommend against testing with a real card. Banks may flag the activity as fraud, block the card, or restrict your account.
Any testing you do is entirely at your own risk. I take no responsibility for any actions you take or any consequences that follow.
Is your app affected?
You can determine if your application is vulnerable by following these steps:
- Search for the event name: Check if your application processes the
customer.subscription.createdwebhook event by searching your codebase for that specific string. - Evaluate: If your application does not process this event, you are likely safe from this specific vulnerability.
- Verify status checks: If you do process this event, confirm whether your
code checks the Stripe subscription
statusattribute. You want to ensure that access is granted only when the subscriptionstatusis explicitly set toactive.
This last step is the most critical. If your database stores the subscription
as active regardless of the Stripe status, your application likely is
vulnerable to the
Impact
Knock-to-Unlock is a high-risk billing logic vulnerability that allows an attacker to access a paid subscription plan without paying.
To be clear, this is not an infrastructure exploit - it does not lead to server takeover or remote code execution.
How long does the free access last?
If a bad actor gets free paid access via
Automatically revoking free access requires a nuanced webhook flow:
- The exploit creates subscription objects in Stripe (status
incomplete) and in the application. - Stripe expires incomplete subscriptions after 23 hours - docs.
- Stripe subscription expiration generates
customer.subscription.updatedevent with subscription statusincomplete_expiredandcanceled_atattribute set.
Will a
Look,if your app is vulnerable, focus on fixing the root cause rather than worrying about how to revoke access after the fact.
Why this mistake is easy to make
Before 3DS became the standard in 2019, implementing Stripe was simpler. Back
then, the customer.subscription.created webhook was the standard way for
granting access and it worked great.
But 3DS changed the rules. Billing actions like creating a subscription, upgrading a plan, or setting up a card can now take up to 23 hours while waiting for user authentication. This shift requires a total rethink of application billing logic, yet many integrations are still stuck in the past.
Here is how Knock-to-Unlock usually sneaks into a codebase:
Legacy update: An older app adds 3DS support but fails to update its webhook logic. It assumes that if Stripe sends a "subscription created" event, the payment is a completed, without ever checking if the subscription
statusis actuallyactive.Copy-paste mistake: Developers building new apps often reference older, "proven" implementations. If they copy a webhook handler that relies on
customer.subscription.created, they inherit a legacy vulnerability in a brand-new system.
The biggest reason this vulnerability persists is that it looks correct. If a developer sees an event named "subscription.created", it seems logical to create an active subscription. With 3DS, "subscription created" no longer means "subscription paid". That gap is exactly where the exploit lives.
Solution
Luckily, the fix for this problem is very easy.
If you're using Stripe hosted Checkout replace customer.subscription.created with
checkout.session.completed. This is also what Stripe recommends in their
subscriptions guide.
If you're using custom checkout, in most cases replacing
customer.subscription.created with invoice.paid webhook event should fix
the problem.
In any case, my recommendation is to avoid customer.subscription.created
altogether. This reduces the risk that a hasty future change by you,
AI, or a junior developer introduces a vulnerability.
Discovery
I have first discovered this vulnerability on SupeRails.com in September 2024. I reported it to the owner right away, and they fixed it within a couple weeks.
This was my first successful discovery of a real-world Stripe integration vulnerability.
How RailsBilling handles Knock-to-Unlock?
RailsBilling is a premium Ruby gem built to simplify complex Stripe subscription integrations. It handles advanced billing models, such as managing multiple plans per customer, and provides several specific benefits:
- Fast deployment: Complete your entire Stripe integration in just a few hours.
- Streamlined checkout: A single-step payment form that combines application data and payment fields into a smooth experience for the customer.
- Built-in best practices: Designed to automatically handle common Stripe "gotchas" and edge cases.
Regarding the Knock-to-Unlock vulnerability: RailsBilling is secure by design.
We grant access only on invoice.paid webhook event, while
customer.subscription.created is not used at all.
Conclusion
Knock-to-Unlock is one of the most severe vulnerabilities found in Stripe integrations. It allows an attacker to gain full, permanent access to a paid application without ever completing a payment or adding a card on file.
This issue usually happens with outdated Stripe integrations that don't account for modern 3D Secure payment requirements.
Fortunately, the solution is straightforward: switch your webhook logic to
listen for checkout.session.completed instead of
customer.subscription.created.
This is just one of many common Stripe pitfalls. If you think your application is at risk, . We offer billing audits to check your app's health, alongside expert billing design help and fixes.
Got questions? Found another Stripe bug? Email me