Unsigned & Delivered: Forging Stripe Events in n8n (CVE-2026-21894)
Jan 8, 2026·7 min read
Executive Summary (TL;DR)
n8n forgot to verify the signature on incoming Stripe webhooks. If an attacker discovers the webhook URL (which contains a UUID), they can send a fake `charge.succeeded` event via `curl`. n8n will blindly trust it, potentially shipping physical goods, unlocking digital licenses, or messing up your financial books. The fix involves implementing standard HMAC-SHA256 signature verification.
A critical logic flaw in n8n's Stripe Trigger node allowed unauthenticated attackers to spoof webhook events, triggering arbitrary workflows by simply knowing the webhook URL.
The Hook: Automation Without Authentication
We live in the golden age of "Glue Code." Tools like n8n allow developers and non-technical users alike to stitch together disparate APIs into powerful workflows. Want to add a row to Google Sheets every time someone pays you on Stripe? Easy. Want to slack your team when a subscription is cancelled? Done in five clicks. The magic behind this real-time interactivity is the Webhook: a mechanism where Service A (Stripe) pushes data to Service B (n8n) immediately.
But here is the catch: Webhooks are just HTTP POST requests sent to a public URL. When you spin up a webhook listener, you are essentially opening a door to the entire internet. To distinguish between a legitimate knock from Stripe and a malicious kick from a hacker, you need a secret handshake. In the cryptography world, we call this an HMAC signature.
In versions of n8n prior to 2.2.2, the developers forgot the handshake. They built a system that relies entirely on the secrecy of the URL itself. This is a classic "Security by Obscurity" failure. It is the digital equivalent of hiding your house key under the doormat and assuming your home is secure because nobody knows which mat is yours. Once that URL leaks—and URLs always leak—the game is over.
The Flaw: Trusting the Envelope
The vulnerability lies in the StripeTrigger.node.ts component. When you configure a Stripe trigger in n8n, the application generates a webhook URL that looks something like this: https://n8n.company.com/webhook/550e8400-e29b-41d4-a716-446655440000. That UUID at the end is the only thing standing between an attacker and your internal logic. The assumption was that the UUID is high-entropy enough that it cannot be guessed. While mathematically true, it ignores the reality that URLs are logged in proxies, browser history, and sometimes even shared in screenshots.
Stripe, being a financial giant, knows the internet is a hostile place. They include a header called stripe-signature in every request. This header contains a timestamp and a signature generated using a shared secret (HMAC-SHA256). The receiving server is supposed to take the payload, hash it with its copy of the secret, and compare the result. If they match, the request is genuine. If they don't, it's garbage.
n8n simply didn't check. The code accepted any JSON payload sent to the correct endpoint. It didn't look for the stripe-signature header, nor did it validate the timestamp to prevent replay attacks. It just took the data and passed it downstream. This meant that anyone with the URL could become the bank.
The Code: Missing the Verification
Let's look at the "smoking gun." In the vulnerable version, the request handling logic was dangerously optimistic. It implicitly trusted that if a request arrived at the specific UUID endpoint, it must be valid. There was zero cryptographic validation code present in the webhook method.
The fix, introduced in PR #22764, finally implements the verification logic we expect from financial software. The developers added a helper function, verifySignature, which performs the necessary cryptographic gymnastics. Here is a breakdown of the patched logic:
// The Fix: actual cryptography
export async function verifySignature(this: IWebhookFunctions): Promise<boolean> {
// 1. Get the secret from credentials
const credential = await this.getCredentials('stripeApi');
// 2. Extract the header
const signature = this.getRequestObject().header('stripe-signature');
// 3. Parse timestamp and signature hash
// ... (parsing logic) ...
// 4. Prevent Replay Attacks (300s window)
const currentTimestamp = Math.floor(Date.now() / 1000);
if (Math.abs(currentTimestamp - parseInt(timestamp, 10)) > 300) return false;
// 5. Calculate HMAC and compare
const signedPayload = `${timestamp}.${req.rawBody}`;
const hmac = createHmac('sha256', credential.signatureSecret);
// ... comparison ...
}[!NOTE] Notice the use of
timingSafeEqual. This is crucial. A standard string comparison returnsfalseas soon as it hits a mismatching character, which allows attackers to guess the signature character-by-character based on response time.timingSafeEqualtakes the same amount of time regardless of the input.
The Exploit: Becoming the Bank
Exploiting this requires two steps: Reconnaissance and Execution. The difficult part is obtaining the webhook UUID. Since n8n creates unique endpoints for triggers, you can't just scan the internet for a default path. However, in a corporate environment, these URLs often leak. A developer might paste one into a Jira ticket, it might appear in a shared screen during a Zoom call, or it might be visible in client-side Javascript if the n8n instance is improperly secured.
Once the UUID is known, the actual exploitation is trivially easy. You don't need to bypass a WAF or perform complex heap grooming. You just need curl. Let's say we want to trigger a workflow that emails a license key when a payment succeeds. We construct a fake Stripe event:
curl -X POST https://target-n8n.com/webhook/LEAKED-UUID-HERE \
-H "Content-Type: application/json" \
-d '{
"id": "evt_fake_123",
"type": "charge.succeeded",
"data": {
"object": {
"id": "ch_fake_123",
"amount": 500000,
"currency": "usd",
"customer": "cus_hacker_007"
}
}
}'The n8n instance receives this, sees charge.succeeded, and dutifully executes the rest of the workflow. The system thinks it just made $5,000. In reality, it just gave away product for free.
The Impact: Business Logic Chaos
The technical impact is an integrity violation, but the business impact is where this hurts. Because n8n is an automation tool, the consequences depend entirely on what the user built. If the workflow simply logs transactions to a text file, the impact is low (messy logs).
However, users typically use n8n for critical operations: provisioning accounts, sending API keys, updating CRM records, or triggering physical shipping logic. An attacker could force the system to mark unpaid invoices as paid, delete customer subscriptions (Denial of Service), or exfiltrate data by triggering workflows that push information to external webhooks controlled by the attacker.
Furthermore, this attack bypasses all auditing on the Stripe side. If you look at your Stripe Dashboard, you see nothing. If you look at your n8n logs, you see a "successful" transaction. This discrepancy makes debugging a nightmare and forensic analysis difficult unless you are specifically looking for unsigned requests.
The Fix: Upgrade and Rotate
The remediation is straightforward: Update to version 2.2.2 immediately. But software updates alone won't save you if your secrets are stale. Because the vulnerability allowed requests without verification, existing workflows might still be configured without a Webhook Signing Secret in the n8n credentials manager.
After patching, you must go into your Stripe Dashboard, find the webhook signing secret for your endpoint, and add it to your n8n Stripe credentials. The new code explicitly checks for this secret; if it's missing, it might default to insecure behavior or fail open depending on configuration, so explicit configuration is key.
Finally, if you suspect your webhook URL has ever been exposed, rotate the UUID. In n8n, this usually means deleting the trigger node and adding a new one, then updating the URL in Stripe. It is a pain, but it is better than letting the internet dictate your business logic.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
n8n n8n.io | >= 0.150.0 < 2.2.2 | 2.2.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-290 |
| Attack Vector | Network (Remote) |
| CVSS | 6.5 (Medium) |
| Authentication | None (Bypassed) |
| Impact | High Integrity / Business Logic Manipulation |
| Patch Status | Fixed in 2.2.2 |
MITRE ATT&CK Mapping
The software receives input from an upstream component, but it does not sufficiently verify that the input is from the intended source.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.