Skip to content

[Segurança] Parâmetro continue do login pode virar caminho protocol-relative e produzir redirecionamento externo #1129

Description

@elio-neto

Problema

O parâmetro continue de /auth/authorize é concatenado no caminho do callback. No retorno do provedor,
o handler de callback recorta o prefixo /auth/callback do pathname e usa o resto como destino do
redirect(). Nem route() nem strip() rejeitam caminhos que começam com duas barras, e um Location
protocol-relative faz o navegador sair do domínio.

Por que é explorável

Com continue=//host-externo:

  1. callbackUrl vira https://opencode.ai/auth/callback//host-externo — ainda mesma origem, porque a
    referência relativa começa com ./callback e não pode trocar de host.
  2. No retorno, next vira //host-externo.
  3. route(locale, "//host-externo") devolve a string inalterada.
  4. redirect("//host-externo") emite Location: //host-externo, e o navegador vai para
    https://host-externo.

Condição de explorabilidade: depende de o emissor OpenAuth aceitar o redirect_uri com o caminho
estendido. Não foi possível confirmar esse aceite lendo apenas este repositório — a validação vive na
dependência @openauthjs/openauth. Vale reproduzir antes de dimensionar o esforço.

Evidência

packages/console/app/src/routes/auth/authorize.ts:4-9:

export async function GET(input: APIEvent) {
  const url = new URL(input.request.url)
  const cont = url.searchParams.get("continue") ?? ""
  const callbackUrl = new URL(`./callback${cont}`, input.request.url)     // <-- concatenacao
  const result = await AuthClient.authorize(callbackUrl.toString(), "code")
  return Response.redirect(result.url, 302)
}

packages/console/app/src/routes/auth/[...callback].ts:35-36:

const next = url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", "")
return redirect(route(locale, next))

packages/console/app/src/lib/language.ts:201-209 — nenhuma validação do formato do caminho:

export function route(locale: Locale, pathname: string) {
  const next = strip(pathname)
  if (next.startsWith("/docs")) return docs(locale, next)
  if (next.startsWith("/auth")) return next
  if (next.startsWith("/workspace")) return next
  if (locale === "en") return next
  if (next === "/") return `/${locale}`
  return `/${locale}${next}`
}

Impacto

Redirecionamento aberto a partir de um domínio confiável, no fim de um fluxo de login — o contexto em que
a vítima esta mais propensa a confiar na página de destino e a digitar credencial.

Não há vazamento de token no fragmento nem no query neste fluxo: o código é trocado no servidor antes do
redirect.

Sugestão de correção

  1. Validar next antes de redirecionar: aceitar apenas caminhos que comecem com uma única barra e não
    com // nem /\.
  2. Melhor: não derivar o destino do pathname. Guardar o continue em uma allowlist de rotas conhecidas
    ou na sessão, e no callback resolver por chave em vez de concatenação de string.
  3. Aplicar a mesma validação dentro de route(), para cobrir os outros chamadores.

Critérios de aceite

  • route() e o redirect do callback rejeitam destinos que comecem com // ou /\
  • O destino pós-login vem de allowlist ou da sessão, não do pathname
  • Teste: /auth/authorize?continue=//exemplo.inválido não produz Location externo
  • Reprodução registrada: o emissor OpenAuth aceita ou recusa o redirect_uri estendido

Labels sugeridas: security, baixa

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