Problema
A área de cobrança inteira é tratada como área de admin no frontend, em três portões distintos. No
servidor, nenhuma das operações correspondentes chama Actor.assertAdmin().
O grep de assertAdmin em packages/console retorna 8 chamadas — todas em user.ts, model.ts,
provider.ts e workspace.ts (update). Nenhuma em billing.ts.
Por que é explorável
As operações são server functions do SolidStart com id estável (billing.setMonthlyLimit,
billing.redeemCoupon, billing.reload, checkoutUrl, sessionUrl, enroll, setUseBalance,
cancelWaitlist). Várias estão ligadas a <form method="post"> para funcionar sem JavaScript, portanto
aceitam POST comum. Um membro com papel member as invoca diretamente, sem precisar que a UI apareca.
Evidência
Portões do frontend:
// packages/console/app/src/routes/workspace/[id]/billing/index.tsx:20-31
<Show when={sessionInfo()?.isAdmin}>
<Show when={isBlack()}><BlackSection /></Show>
<BillingSection />
<RedeemSection />
<Show when={billingInfo()?.customerID}>
<ReloadSection />
<MonthlyLimitSection />
<PaymentSection />
</Show>
</Show>
// packages/console/app/src/routes/workspace/[id].tsx:34-41 (nav desktop) e :62-69 (nav mobile)
<Show when={userInfo()?.isAdmin}>
<A href={`/workspace/${params.id}/billing`} ...>{i18n.t("workspace.nav.billing")}</A>
<A href={`/workspace/${params.id}/settings`} ...>{i18n.t("workspace.nav.settings")}</A>
</Show>
Servidor, sem verificação de papel:
// packages/console/core/src/billing.ts:188-197
export const setMonthlyLimit = fn(z.number(), async (input) => {
return await Database.use((tx) =>
tx.update(BillingTable).set({ monthlyLimit: input })
.where(eq(BillingTable.workspaceID, Actor.workspace())),
)
})
Funções sem assertAdmin em packages/console/core/src/billing.ts:
| Linha |
Função |
Efeito ao ser chamada por membro comum |
| 75 |
reload |
Cobra imediatamente o cartão salvo do workspace |
| 158 |
redeemCoupon |
Resgata cupom em nome do workspace (BUILDATHON credita US$ 500, linha 174) |
| 188 |
setMonthlyLimit |
Altera o teto de gasto de todo o workspace |
| 199 |
generateCheckoutUrl |
Gera sessão de checkout do Stripe |
| 412 |
generateSessionUrl |
Gera URL do portal de cobrança (ver faturas, trocar meio de pagamento) |
| 450 |
subscribeBlack |
Cria assinatura |
Ações de rota na mesma condição:
routes/workspace/[id]/billing/black-section.tsx:117-139 — setUseBalance
routes/workspace/[id]/billing/black-section.tsx:69-87 — cancelWaitlist
routes/workspace/[id]/go/lite-section.tsx:121-141 — setLiteUseBalance
Impacto
Escalada de privilégio dentro do workspace. Um membro comum:
- altera o limite mensal de gasto e assim libera ou bloqueia o consumo de todo o workspace
- dispara uma cobrança imediata no cartão salvo
- resgata cupons em nome do workspace
- obtém URL do portal de cobrança do Stripe, pela qual vê faturas e troca o meio de pagamento
- entra ou cancela assinatura
Sugestão de correção
- Adicionar
Actor.assertAdmin() no início de reload, redeemCoupon, setMonthlyLimit,
generateCheckoutUrl, generateSessionUrl, generateLiteCheckoutUrl e subscribeBlack.
- Adicionar a mesma verificação nas ações de rota
setUseBalance, setLiteUseBalance e
cancelWaitlist.
- Onde a operação deve ficar disponível para membro (por exemplo assinatura Lite individual),
documentar a decisão no código e restringir o alcance ao próprio usuário em vez de ao workspace.
- Como controle estrutural, exigir que toda função exportada de
billing.ts declare explicitamente o
papel mínimo.
Critérios de aceite
Labels sugeridas: security, alta
Achado da auditoria estatica de seguranca do commit 0465a5d. Relatorio completo em docs/security-audit/relatorio-auditoria-seguranca.pdf.
Problema
A área de cobrança inteira é tratada como área de admin no frontend, em três portões distintos. No
servidor, nenhuma das operações correspondentes chama
Actor.assertAdmin().O grep de
assertAdminempackages/consoleretorna 8 chamadas — todas emuser.ts,model.ts,provider.tseworkspace.ts(update). Nenhuma embilling.ts.Por que é explorável
As operações são server functions do SolidStart com id estável (
billing.setMonthlyLimit,billing.redeemCoupon,billing.reload,checkoutUrl,sessionUrl,enroll,setUseBalance,cancelWaitlist). Várias estão ligadas a<form method="post">para funcionar sem JavaScript, portantoaceitam POST comum. Um membro com papel
memberas invoca diretamente, sem precisar que a UI apareca.Evidência
Portões do frontend:
Servidor, sem verificação de papel:
Funções sem
assertAdminempackages/console/core/src/billing.ts:reloadredeemCouponsetMonthlyLimitgenerateCheckoutUrlgenerateSessionUrlsubscribeBlackAções de rota na mesma condição:
routes/workspace/[id]/billing/black-section.tsx:117-139—setUseBalanceroutes/workspace/[id]/billing/black-section.tsx:69-87—cancelWaitlistroutes/workspace/[id]/go/lite-section.tsx:121-141—setLiteUseBalanceImpacto
Escalada de privilégio dentro do workspace. Um membro comum:
Sugestão de correção
Actor.assertAdmin()no início dereload,redeemCoupon,setMonthlyLimit,generateCheckoutUrl,generateSessionUrl,generateLiteCheckoutUrlesubscribeBlack.setUseBalance,setLiteUseBalanceecancelWaitlist.documentar a decisão no código e restringir o alcance ao próprio usuário em vez de ao workspace.
billing.tsdeclare explicitamente opapel mínimo.
Critérios de aceite
billing.tsdeclara e verifica o papel mínimo no servidorsetUseBalance,setLiteUseBalanceecancelWaitlistverificam o papelmemberrecebe erro de autorizaçãoBillingTableapós tentativa demember<Show when={isAdmin}>do frontend tem um teste de servidor correspondenteLabels sugeridas: security, alta
Achado da auditoria estatica de seguranca do commit
0465a5d. Relatorio completo emdocs/security-audit/relatorio-auditoria-seguranca.pdf.