Problema
Três funções de core carecem de guarda de autorização. Nenhuma delas é explorável no commit auditado,
porque nenhuma rota atual passa a elas dado controlado pelo cliente — mas em todas as três a guarda vive
fora da função, no chamador, e a assinatura aceita qualquer valor. São armadilhas esperando a próxima
tela.
Agrupadas em uma issue por serem o mesmo tema: guarda que deveria estar dentro da função e não está.
12a. Workspace.remove não verifica papel de admin
packages/console/core/src/workspace.ts:68-75
export const update = fn(z.object({ name: z.string().min(1).max(255) }), async ({ name }) => {
Actor.assertAdmin() // <-- presente em update
const workspaceID = Actor.workspace()
...
})
export const remove = fn(z.void(), async () => {
// <-- ausente em remove
await Database.use((tx) =>
tx.update(WorkspaceTable).set({ timeDeleted: sql`now()` })
.where(eq(WorkspaceTable.id, Actor.workspace())),
)
})
Renomear o workspace exige admin; apagá-lo não. A assimetria esta lado a lado no mesmo arquivo, o que
indica esquecimento. O grep de Workspace.remove não encontra chamador hoje.
Risco: a tela de settings tem espaço natural para um botão de excluir workspace. No dia em que ele
existir, qualquer membro apaga o workspace inteiro.
12b. unsubscribeBlack e unsubscribeLite derivam o inquilino do id recebido
packages/console/core/src/billing.ts:515-538 e 540-563
export const unsubscribeBlack = fn(z.object({ subscriptionID: z.string() }), async ({ subscriptionID }) => {
const workspaceID = await Database.use((tx) =>
tx.select({ workspaceID: BillingTable.workspaceID })
.from(BillingTable)
.where(eq(BillingTable.subscriptionID, subscriptionID)) // <-- o id do cliente escolhe o inquilino
.then((rows) => rows[0]?.workspaceID),
)
if (!workspaceID) throw new Error("Workspace ID not found for subscription")
await Database.transaction(async (tx) => {
await tx.update(BillingTable).set({ subscriptionID: null, subscription: null })
.where(eq(BillingTable.workspaceID, workspaceID))
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
})
})
Não há Actor.workspace() em nenhuma linha. O único chamador hoje é
packages/console/app/src/routes/stripe/webhook.ts (linhas 186, 188, 197, 199), que valida a assinatura
do evento em constructEventAsync(..., Resource.STRIPE_WEBHOOK_SECRET.value) nas linhas 15-19 — por isso
não há caminho de exploração.
Risco: subscriptionID é devolvido ao frontend por queryBillingInfo
(routes/workspace/common.tsx:114 e :119). Um botão "cancelar assinatura" que chame essas funções com
o id vindo do cliente — o caminho natural — permite a qualquer usuário autenticado cancelar a assinatura
de qualquer workspace.
12c. Key.create aceita userID arbitrário
packages/console/core/src/key.ts:42-73
export const create = fn(
z.object({ userID: z.string(), name: z.string().min(1).max(255) }),
async (input) => {
...
await Database.use((tx) =>
tx.insert(KeyTable).values({
id: keyID,
workspaceID: Actor.workspace(),
userID: input.userID, // <-- nao validado contra a UserTable nem contra Actor.userID()
name,
key: secretKey,
timeUsed: null,
}),
)
return keyID
},
)
O workspaceID vem do ator (correto), mas o userID não é conferido. O único chamador de rota,
routes/workspace/[id]/keys/key-section.tsx:32, passa
Actor.assert("user").properties.userID — o próprio ator. Os outros chamadores (workspace.ts:44,
user.ts:120 e :176) rodam sob ator system com userID recém-criado pelo servidor.
Risco: uma tela de administração de chaves que receba o userID do cliente permite a um membro criar
chave atribuída a outro usuário, deslocando a atribuição de custo e de limite mensal
(UserTable.monthlyLimit é consultado por chave em routes/zen/util/handler.ts:601-606) e permitindo
consumir a cota do admin. Como o userID não é validado contra o workspace, uma chave pode apontar para
usuário de outro inquilino.
Sugestão de correção
- 12a: adicionar
Actor.assertAdmin() em Workspace.remove. Considerar exigir confirmação por nome
e revogar as chaves do workspace na mesma transação.
- 12b: inverter a responsabilidade — derivar o
subscriptionID do workspace do ator em vez do
contrário. Manter uma variante interna com nome explícito (unsubscribeBlackFromWebhook), comentada
como de uso restrito ao webhook, e uma variante pública que exige ator de usuário, verifica papel e
filtra por Actor.workspace().
- 12c: validar dentro de
Key.create que existe UserTable com aquele id, aquele workspaceID e
timeDeleted nulo; e exigir input.userID === Actor.userID() salvo para ator system ou papel
admin.
Critérios de aceite
Labels sugeridas: security, media
Achado da auditoria estatica de seguranca do commit 0465a5d. Relatorio completo em docs/security-audit/relatorio-auditoria-seguranca.pdf.
Problema
Três funções de core carecem de guarda de autorização. Nenhuma delas é explorável no commit auditado,
porque nenhuma rota atual passa a elas dado controlado pelo cliente — mas em todas as três a guarda vive
fora da função, no chamador, e a assinatura aceita qualquer valor. São armadilhas esperando a próxima
tela.
Agrupadas em uma issue por serem o mesmo tema: guarda que deveria estar dentro da função e não está.
12a.
Workspace.removenão verifica papel de adminpackages/console/core/src/workspace.ts:68-75Renomear o workspace exige admin; apagá-lo não. A assimetria esta lado a lado no mesmo arquivo, o que
indica esquecimento. O grep de
Workspace.removenão encontra chamador hoje.Risco: a tela de settings tem espaço natural para um botão de excluir workspace. No dia em que ele
existir, qualquer membro apaga o workspace inteiro.
12b.
unsubscribeBlackeunsubscribeLitederivam o inquilino do id recebidopackages/console/core/src/billing.ts:515-538e540-563Não há
Actor.workspace()em nenhuma linha. O único chamador hoje épackages/console/app/src/routes/stripe/webhook.ts(linhas 186, 188, 197, 199), que valida a assinaturado evento em
constructEventAsync(..., Resource.STRIPE_WEBHOOK_SECRET.value)nas linhas 15-19 — por issonão há caminho de exploração.
Risco:
subscriptionIDé devolvido ao frontend porqueryBillingInfo(
routes/workspace/common.tsx:114e:119). Um botão "cancelar assinatura" que chame essas funções como id vindo do cliente — o caminho natural — permite a qualquer usuário autenticado cancelar a assinatura
de qualquer workspace.
12c.
Key.createaceitauserIDarbitráriopackages/console/core/src/key.ts:42-73O
workspaceIDvem do ator (correto), mas ouserIDnão é conferido. O único chamador de rota,routes/workspace/[id]/keys/key-section.tsx:32, passaActor.assert("user").properties.userID— o próprio ator. Os outros chamadores (workspace.ts:44,user.ts:120e:176) rodam sob atorsystemcomuserIDrecém-criado pelo servidor.Risco: uma tela de administração de chaves que receba o
userIDdo cliente permite a um membro criarchave atribuída a outro usuário, deslocando a atribuição de custo e de limite mensal
(
UserTable.monthlyLimité consultado por chave emroutes/zen/util/handler.ts:601-606) e permitindoconsumir a cota do admin. Como o
userIDnão é validado contra o workspace, uma chave pode apontar parausuário de outro inquilino.
Sugestão de correção
Actor.assertAdmin()emWorkspace.remove. Considerar exigir confirmação por nomee revogar as chaves do workspace na mesma transação.
subscriptionIDdo workspace do ator em vez docontrário. Manter uma variante interna com nome explícito (
unsubscribeBlackFromWebhook), comentadacomo de uso restrito ao webhook, e uma variante pública que exige ator de usuário, verifica papel e
filtra por
Actor.workspace().Key.createque existeUserTablecom aquele id, aqueleworkspaceIDetimeDeletednulo; e exigirinput.userID === Actor.userID()salvo para atorsystemou papeladmin.Critérios de aceite
Workspace.removechamaActor.assertAdmin()memberrecebe erro eWorkspaceTable.timeDeletedpermanece nulounsubscribeBlack/unsubscribeLitepara chamada por usuário, que filtra porActor.workspace()e verifica papelsubscriptionIDarbitrário tem nome que declara o uso restrito e é chamadasomente pelo webhook
subscriptionIDde outro workspaceKey.createrejeitauserIDque não pertenca ao workspace do atorKey.createrejeitauserIDdiferente do ator, exceto para atorsystemou papeladminuserIDde outro usuário falha para papelmemberLabels sugeridas: security, media
Achado da auditoria estatica de seguranca do commit
0465a5d. Relatorio completo emdocs/security-audit/relatorio-auditoria-seguranca.pdf.