
Reproduce un IDOR CWE-639 en Lago v1.53.0 donde el webhook único de Stripe carga facturas sin alcance de organización, lo que permite eludir el pago entre organizaciones. Incluye un laboratorio de loopback.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · lago-stripe-invoice-idor
Lago v1.53.0 - GetLago
POST /webhooks/stripe/:organization_id verifica Stripe-Signature con el secreto de webhook de esa organización de la URL. Luego la rama one-time en StripeService#update_payment_status carga la factura con Invoice.find_by(id:) y sin organization_id. handle_missing_payment está acotado. Las facturas de Adyen ya pasan organization_id:. Cashfree y Flutterwave copian el find_by(id:) sin acotar de Stripe. Este lab es Stripe.
Otro inquilino en la misma base de datos, con Stripe conectado, puede marcar tu factura como pagada. Su panel de Stripe no tiene ningún cargo coincidente. El tuyo podría.
| ID | sin CVE todavía |
| CWE | CWE-639, CWE-345 |
| CVSS | Alto: 7.7 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N |
| Producto | Lago / lago-api |
| Afectado | hasta v1.53.0 webhook one-time de Stripe; base de datos compartida |
| Auth | organización atacante con Stripe conectado; UUID de factura de la víctima |
| Licencia | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Firmar un payment_intent.succeeded como su organización, poner payment_type=one-time y el lago_invoice_id de tu factura. La ruta acotada nunca se ejecuta. La fila de la víctima pasa a payment_status=succeeded. Se desbloquea la recarga de monedero / la suscripción con pago restringido.
No es no autenticado. No es una shell. El self-host de una sola organización no es este bug: ese secreto de firma ya es dueño de las cuentas por pagar de esa organización. Necesitas un UUID de factura de la víctima (URL del portal, correo, PDF, API filtrada) y una fila stripe_customer de la víctima. Evento ficticio. Sin Stripe en vivo.
Mismo árbol que el acceptInvite ATO. Bug diferente.
Leí update_payment_status, luego el find_by(id:) de create_payment one-time, luego el organization_id: de handle_missing_payment. El comentario en la ruta acotada teme al mismo secreto de Stripe usado entre organizaciones. La rama one-time olvidó tener miedo.
El primer cliente que mire esto hará POST de un payment_intent.succeeded sin payment_type. handle_missing_payment busca la factura y organization_id. Organización atacante, factura de la víctima. nil. Pendiente. Ese es el control. El lab lo verifica.
Caminos equivocados ya registrados: firmar con el secreto de webhook de la víctima (entonces ya eres dueño de las cuentas por pagar de ese comerciante); self-host de una sola organización; UUID de factura de la organización atacante (pago dentro de la organización); sin stripe_customer en la víctima (excepción, no succeeded); tratar el HTTP 200 del webhook como el oráculo (el job es asíncrono - consulta GET /api/v1/invoices/:id); una reverse shell. Teatro.
Luego: dos llamadas a registerUser plantan Victim Corp y Attacker Corp. La clave REST de la víctima crea un add-on, un cliente y una factura impagada con skip_psp para no necesitar un PSP en vivo para crear la fila. seed_stripe.rb planta proveedores de Stripe y un stripe_customer en la víctima. webhook_secret ficticio. sk_test_lab_* ficticio. Evento de control firmado, sin payment_type: sigue pendiente. Evento firmado con payment_type=one-time: succeeded, paid=1000.
cd lab
./run.sh
Objetivo solo http://127.0.0.1:13001. Invite ATO usó 13000. Este no comparte volumen con ese lab.
IOC payment_status_before=pending
IOC control webhook (no payment_type) payment_status_after_control=pending
IOC attack webhook payment_type=one-time
IOC poll payment_status=succeeded paid=1000
SUCCESS Lago Stripe one-time webhook unscoped invoice
Invoice.find_by(id:, organization_id:) en la ruta one-time, tal como ya lo hacen handle_missing_payment y las facturas de Adyen. Lo mismo para Cashfree/Flutterwave.
webhooks_controller.rb · stripe_service.rb · adyen_service.rb · cashfree_service.rb · flutterwave_service.rbGNU Affero GPL v3.0. Ver LICENSE. Solo lab en loopback. Sin garantía.