Skip to content

[Segurança] POST /bench/submission grava sem autenticação e sem validar o JSON que a página pública desserializa #1126

Description

@elio-neto

Problema

O endpoint de submissão de benchmark não tem autenticação, nem token compartilhado, nem rate limit, e não
valida o formato de body.result — o único teste é "string não vazia". A página pública /bench pega as
100 linhas mais recentes e faz JSON.parse(row.result) sem try/catch, seguido de iteração em
parsed.tasks.

Por que é explorável

Uma única submissão com result: "x" faz o JSON.parse lançar. Uma com result: "{}" faz
parsed.tasks ser undefined e o for...of lançar. Como a consulta é ordenada por timeCreated desc
com limit 100, a linha ruim permanece no conjunto e a página fica quebrada até alguém apagar a linha
diretamente no banco.

Evidência

packages/console/app/src/routes/bench/submission.ts:14-31:

export async function POST(event: APIEvent) {
  const dict = i18n(localeFromRequest(event.request))
  const body = (await event.request.json()) as SubmissionBody

  if (!body.model || !body.agent || !body.result) {
    return Response.json({ error: dict["bench.submission.error.allFieldsRequired"] }, { status: 400 })
  }

  await Database.use((tx) =>
    tx.insert(BenchmarkTable).values({
      id: Identifier.create("benchmark"),
      model: body.model,
      agent: body.agent,
      result: body.result,        // <-- string arbitraria
    }),
  )

  return Response.json({ success: true }, { status: 200 })
}

packages/console/app/src/routes/bench/index.tsx:15-31 — a leitura sem proteção:

const rows = await Database.use((tx) =>
  tx.select().from(BenchmarkTable).orderBy(desc(BenchmarkTable.timeCreated)).limit(100),
)
return rows.map((row) => {
  const parsed = JSON.parse(row.result) as BenchmarkResult    // <-- sem try/catch
  const taskScores: Record<string, number> = {}
  for (const t of parsed.tasks) {                             // <-- parsed.tasks pode ser undefined
    taskScores[t.task.id] = t.averageScore
  }
  ...
})

packages/console/app/src/routes/bench/[id].tsx:81-88 — o mesmo padrão na página de detalhe.

Impacto

  • Negação de serviço persistente de uma página pública com uma única requisição.
  • Poluição do leaderboard: qualquer pessoa insere resultados arbitrários de qualquer modelo e agente,
    que aparecem como dados oficiais do projeto em /bench.
  • Crescimento não limitado da BenchmarkTable: o campo result não tem teto de tamanho na rota.

Sugestão de correção

  1. Exigir credencial no endpoint — um token de CI comparado com safeEqual, que já existe em
    packages/console/core/src/util/crypto.ts — ou mover a ingestão para um job autenticado.
  2. Validar body.result com um schema zod do formato BenchmarkResult antes do insert, recusando o que
    não casar.
  3. Blindar a leitura: envolver o JSON.parse em try/catch e descartar linhas inválidas em vez de
    quebrar a página.
  4. Adicionar limite de tamanho ao campo result e rate limit por IP.

Critérios de aceite

  • POST /bench/submission exige credencial e responde 401 sem ela
  • body.result é validado por schema zod (tasks presente e array) antes do insert
  • bench/index.tsx e bench/[id].tsx toleram linha inválida sem quebrar a renderização
  • Teste: submissão com result inválido é recusada
  • Teste: linha inválida já existente no banco não derruba /bench
  • Limpeza das linhas inválidas já existentes, se houver

Labels sugeridas: security, media

Achado da auditoria estatica de seguranca do commit 0465a5d. Relatorio completo em docs/security-audit/relatorio-auditoria-seguranca.pdf.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions