Conversation
shouldAllowUnsignedPayload skipped Stripe signature checking on the public webhook endpoint whenever NODE_ENV was "development" or "test", as long as a request carried x-paykit-cloud-replay: 1. That header has no secret behind it, so on any deployment where NODE_ENV isn't exactly "production" (self-hosted containers that never set it, staging environments, anything misconfigured), anyone could POST a forged Stripe event body with that one header and have it processed as fully trusted: fake subscriptions, invoices, payments. Drop the NODE_ENV checks and keep only the explicit opt-in env vars that already exist for this (PAYKIT_ALLOW_UNSIGNED_PAYLOADS and its legacy alias), so the replay feature still works for anyone who turns it on deliberately. Added a unit test for shouldAllowUnsignedPayload covering the header requirement, the removed NODE_ENV paths, and both opt-in flags.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughUnsigned webhook payloads now require the cloud-replay header and an explicit opt-in environment variable. Development and test modes no longer grant access by themselves. Tests cover these conditions. ChangesUnsigned payload handling
Priority: ⬆️ High Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to Unsigned webhooks now require explicit opt-in rather than development or test mode alone. No actionable merge-blocking risk remains in the supplied change context; merge after normal checks pass. Security Architecture ReviewSecurity architecture risk: ⚪ Minimal · up to The change tightens webhook authentication without expanding authority or changing event-processing behavior. Unsigned replay remains an existing, explicit deployment opt-in rather than being enabled automatically by development or test mode. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit checks the replay sign, Comment |
security.api_token wasn't the issue here, this is in the webhook route:
shouldAllowUnsignedPayload(packages/paykit/src/webhook/webhook.api.ts) skips Stripe signature verification onPOST /webhookwhenever a request carriesx-paykit-cloud-replay: 1andNODE_ENVis"development"or"test".The header has no secret behind it, it's just a magic string. So on any deployment where
NODE_ENVisn't exactly"production"(a self-hosted container that never sets it, a staging environment, anything misconfigured), an external request can send that one header along with an arbitrary JSON body and have it parsed byparseUnsignedStripeEventand processed as a fully trusted Stripe event, no signature, nowebhookSecretinvolved at all. That flows straight intohandleWebhook/processWebhookEventand applies subscription/invoice/payment/payment-method actions to the database. Their ownstripe-provider.test.tsalready demonstratesallowUnsignedPayload: trueaccepts an arbitrary event with zero verification, which is the intended behavior for that flag; the gap is what's allowed to set it totrue.Two explicit opt-in env vars already exist for the legitimate "cloud replay" use case (
PAYKIT_ALLOW_UNSIGNED_PAYLOADS, and its legacy aliasPAYKIT_ALLOW_STALE_SIGNATURES). This PR removes the twoNODE_ENVchecks and keeps only those, so the replay feature still works for anyone who turns it on deliberately, but an ambientNODE_ENVvalue nobody thought of as a security setting can no longer disable webhook auth.Added
webhook.api.test.tscovering the header requirement, thatNODE_ENV=developmentandNODE_ENV=testalone no longer allow it, and that both opt-in flags still do. Confirmed those two cases fail against the current code and pass after the fix.Ran
test:unit: 148 passed, 1 pre-existing unrelated failure (a Windows shell-quoting difference incli/listen.test.ts, present on main before this change too, nothing to do with this). Also ranoxlint --deny-warningsandoxfmt --checkon the changed files (clean) andtsc --buildfor the paykit package (clean).Summary by cubic
Closes a webhook authentication bypass where a request carrying an
x-paykit-cloud-replay: 1header could skip Stripe signature verification onPOST /webhookwheneverNODE_ENVwas"development"or"test". The header has no secret, so forged billing events (subscriptions, invoices, payments) could be processed as fully trusted on any deployment whereNODE_ENVwasn't exactly"production". Unsigned payloads now require an explicit opt-in viaPAYKIT_ALLOW_UNSIGNED_PAYLOADS(or legacy aliasPAYKIT_ALLOW_STALE_SIGNATURES), and theNODE_ENVchecks are removed.Behavior
NODE_ENVvalues to allow unsigned replay must now set one of the two opt-in flags.NODE_ENVpaths (which fail against the old code), and both opt-in flags.Written for commit b2bfdea. Summary will update on new commits.
Summary by CodeRabbit