
Reproduz um IDOR CWE-639 no Lago v1.53.0, onde o webhook one-time do Stripe carrega faturas sem escopo de organização, permitindo bypass de pagamento entre organizações. Inclui um laboratório 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 com o segredo de webhook daquela organização da URL. Em seguida, o branch one-time em StripeService#update_payment_status carrega a fatura com Invoice.find_by(id:) e sem organization_id. handle_missing_payment é escopado. Faturas Adyen já passam organization_id:. Cashfree e Flutterwave copiam o find_by(id:) sem escopo do Stripe. Este lab é o Stripe.
Outro tenant no mesmo banco de dados, com Stripe conectado, pode marcar sua fatura como paga. O dashboard Stripe deles não tem cobrança correspondente. O seu pode ter.
| ID | sem CVE ainda |
| 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 |
| Produto | Lago / lago-api |
| Afetado | até v1.53.0 webhook one-time do Stripe; banco de dados compartilhado |
| Auth | organização atacante com Stripe conectado; UUID da fatura da vítima |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1 |
Assinar um payment_intent.succeeded como sua organização, colocar payment_type=one-time e o lago_invoice_id da sua fatura. O caminho escopado nunca é executado. A linha da vítima se torna payment_status=succeeded. Recarga de carteira / assinatura com pagamento obrigatório é desbloqueada.
Não é não autenticado. Não é um shell. Self-host de organização única não é este bug: esse segredo de assinatura já é dono dos pagáveis daquela organização. Você precisa de um UUID de fatura da vítima (URL do portal, e-mail, PDF, API vazada) e de uma linha stripe_customer da vítima. Evento fictício. Sem Stripe ao vivo.
Mesma árvore do acceptInvite ATO. Bug diferente.
Eu li update_payment_status, depois o find_by(id:) do create_payment one-time, depois o organization_id: do handle_missing_payment. O comentário no caminho escopado teme o mesmo segredo Stripe usado entre organizações. O branch one-time esqueceu de temer.
O primeiro cliente que olhar para isso vai fazer POST de um payment_intent.succeeded sem payment_type. handle_missing_payment busca a fatura e organization_id. Organização atacante, fatura da vítima. nil. Pendente. Esse é o controle. O lab afirma isso.
Caminhos errados já registrados: assinar com o segredo de webhook da vítima (aí você já é dono dos pagáveis daquele comerciante); self-host de organização única; UUID de fatura da organização atacante (pagamento intra-organização); sem stripe_customer na vítima (exceção, não succeeded); tratar o HTTP 200 do webhook como o oráculo (o job é assíncrono - faça polling em GET /api/v1/invoices/:id); um reverse shell. Teatro.
Então: duas chamadas registerUser plantam Victim Corp e Attacker Corp. A chave REST da vítima cria um add-on, um customer e uma fatura não paga com skip_psp para que eu não precisasse de um PSP ao vivo para criar a linha. seed_stripe.rb planta provedores Stripe e um stripe_customer na vítima. webhook_secret fictício. sk_test_lab_* fictício. Evento de controle assinado, sem payment_type: ainda pendente. Evento assinado com payment_type=one-time: succeeded, paid=1000.
cd lab
./run.sh
Alvo apenas http://127.0.0.1:13001. O Invite ATO usou 13000. Este não compartilha volume com aquele 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:) no caminho one-time, do jeito que handle_missing_payment e as faturas Adyen já fazem. O mesmo para Cashfree/Flutterwave.
webhooks_controller.rb · stripe_service.rb · adyen_service.rb · cashfree_service.rb · flutterwave_service.rbGNU Affero GPL v3.0. Veja LICENSE. Apenas lab em loopback. Sem garantia.