T09 · Insecure Skill Coding Practices
- Location
plantillas/app/api/webhooks/stripe/route.ts:28- Finding
Stripe Events and Subscriptions Are Not Scoped to the Product Authorizing Application Access
- Content
View full analysis
Vulnerability Details
File Location:
plantillas/app/api/webhooks/stripe/route.ts:28-38plantillas/app/api/cron/refresco/route.ts:25-31
Vulnerability Type: Improper authorization caused by missing Stripe product and price validation
Risk Level: HighVulnerable Code
Webhook access grant:
ts if (evento.type === 'checkout.session.completed') { const s = evento.data.object const email = normalizarCorreo(s.customer_details?.email ?? s.customer_email) if (!EMAIL_REGEX.test(email)) return new Response('Sin correo en el pago.', { status: 200 }) const { data: lote, error: e1 } = await svc.from('lotes_miembros') .insert({ hecho_por: 'stripe', origen: 'pasarela', agregados: 1, nota: `pago: ${email}` }).select('id').single() if (e1 || !lote) return new Response('No pude crear el lote.', { status: 500 }) const { error } = await svc.from('miembros_activos') .upsert({ email, nombre: s.customer_details?.name ?? null, lote_id: lote.id, origen: 'pasarela' })Scheduled subscription reconciliation:
ts for (const status of ['active', 'trialing', 'past_due'] as const) { for await (const sub of stripe.subscriptions.list({ status, limit: 100, expand: ['data.customer'] })) { const c = sub.customer as Stripe.Customer | Stripe.DeletedCustomer if ('email' in c && c.email) vistos.set(normalizarCorreo(c.email), c.name ?? null) } } return [...vistos].map(([email, nombre]) => ({ email, nombre }))Technical Analysis
The webhook verifies Stripe’s signature, which establishes that the event originated from Stripe. However, signature verification does not establish that the Checkout Session concerns the specific product or price that should authorize access to this application.
For every
checkout.session.completedevent delivered to the endpoint, the handler extracts the customer email and inserts it into the privilegedmiembros_activostable through a Supabase service-role client. It does not inspect the Checko ...[truncated 2192 chars]- Remediation
View remediation
Remediation Suggestions
-
Define an explicit allowlist of Stripe product IDs or price IDs that authorize access to this application. Store this configuration in server-only environment variables or another protected server-side configuration source.
-
For
checkout.session.completed, retrieve and inspect the Checkout Session line items before granting access. Require at least one line item whose product or price ID matches the application’s entitlement allowlist. -
Validate the relevant payment state before granting access. The handler should confirm that the event represents the expected successful payment or subscription state for the selected payment method.
-
Scope scheduled reconciliation to authorized products or prices. Enumerate subscription items and retain only subscriptions containing an allowed price ID rather than importing every subscription in the Stripe account.
-
Apply the same product and price scoping to cancellation processing so an unrelated subscription event cannot incorrectly alter application membership.
-
Prefer recording a stable entitlement mapping containing the Stripe customer, subscription, product, and price identifiers. Use that mapping when granting, reconciling, or revoking access instead of relying only on the customer email address.
-
Add automated security tests covering:
- An authorized product grants access.
- An unrelated product does not grant access.
- A cheaper product in the same Stripe account does not grant access.
- Reconciliation excludes unrelated active subscriptions.
- Cancellation of an unrelated subscription does not alter a valid application entitlement.
-
