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:
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.
- No retorno,
next vira //host-externo.
route(locale, "//host-externo") devolve a string inalterada.
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
- Validar
next antes de redirecionar: aceitar apenas caminhos que comecem com uma única barra e não
com // nem /\.
- 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.
- Aplicar a mesma validação dentro de
route(), para cobrir os outros chamadores.
Critérios de aceite
Labels sugeridas: security, baixa
Achado da auditoria estatica de seguranca do commit 0465a5d. Relatorio completo em docs/security-audit/relatorio-auditoria-seguranca.pdf.
Problema
O parâmetro
continuede/auth/authorizeé concatenado no caminho do callback. No retorno do provedor,o handler de callback recorta o prefixo
/auth/callbackdopathnamee usa o resto como destino doredirect(). Nemroute()nemstrip()rejeitam caminhos que começam com duas barras, e umLocationprotocol-relative faz o navegador sair do domínio.
Por que é explorável
Com
continue=//host-externo:callbackUrlvirahttps://opencode.ai/auth/callback//host-externo— ainda mesma origem, porque areferência relativa começa com
./callbacke não pode trocar de host.nextvira//host-externo.route(locale, "//host-externo")devolve a string inalterada.redirect("//host-externo")emiteLocation: //host-externo, e o navegador vai parahttps://host-externo.Condição de explorabilidade: depende de o emissor OpenAuth aceitar o
redirect_uricom o caminhoestendido. 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:packages/console/app/src/routes/auth/[...callback].ts:35-36:packages/console/app/src/lib/language.ts:201-209— nenhuma validação do formato do caminho: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
nextantes de redirecionar: aceitar apenas caminhos que comecem com uma única barra e nãocom
//nem/\.pathname. Guardar ocontinueem uma allowlist de rotas conhecidasou na sessão, e no callback resolver por chave em vez de concatenação de string.
route(), para cobrir os outros chamadores.Critérios de aceite
route()e o redirect do callback rejeitam destinos que comecem com//ou/\pathname/auth/authorize?continue=//exemplo.inválidonão produzLocationexternoredirect_uriestendidoLabels sugeridas: security, baixa
Achado da auditoria estatica de seguranca do commit
0465a5d. Relatorio completo emdocs/security-audit/relatorio-auditoria-seguranca.pdf.