Vulnerabilidade de redirecionamento aberto em callbacks de autenticação: como corrigir
Aprenda como uma vulnerabilidade de redirecionamento aberto aparece em callbacks de autenticação, como atacantes a exploram e como corrigir redirecionamentos com allowlists rígidas e parsing seguro de URLs.

Por que redirecionamentos em fluxos de login podem virar um problema de segurança
Um redirecionamento é uma instrução que diz ao navegador: "vá para essa outra página." Em fluxos de login, redirecionamentos tornam a experiência fluida: seu app envia alguém para entrar e então retorna a pessoa para o que estava tentando ver.
A etapa de "retornar pra mim" geralmente vem em um parâmetro como returnTo, next ou redirect. Se seu app aceita qualquer URL ali, você tem uma vulnerabilidade de redirecionamento aberto.
Isso aparece constantemente em protótipos porque redirecionamentos são uma vitória fácil para uma demo mais suave. Código gerado por IA ou apressado frequentemente prioriza "fazer funcionar" e ignora regras de segurança chatas.
O risco é maior do que "o usuário acaba na página errada." Seu domínio vira um ponto de confiança usado para enviar pessoas a um site controlado por atacantes. O link parece legítimo porque começa no seu domínio real e muitas vezes mostra uma tela de login familiar.
Um fluxo de phishing realista funciona assim: alguém clica em um link de "Entrar" vindo de um e-mail ou chat, cai na sua página de login real, autentica, e então é redirecionado para uma tela falsa convincente que pede para "entrar novamente", digitar um código MFA ou confirmar detalhes de pagamento. Muitos usuários não percebem porque tudo parecia normal até o passo final.
O que são redirecionamentos abertos e callbacks de autenticação
Um redirecionamento aberto é quando seu app deixa uma entrada não confiável decidir para onde o usuário será enviado em seguida. O exemplo clássico é uma URL como https://yourapp.com/redirect?to=..., onde to pode ser qualquer site.
Um callback de autenticação é a página para a qual seu provedor de identidade retorna após o login. Depois que um usuário entra com Google, GitHub ou outro provedor, o provedor o envia de volta ao seu app em uma URL de callback para que seu app termine o login e crie uma sessão.
O problema começa quando você combina os dois.
Um padrão comum:
- Um usuário tenta acessar
/billing. - Seu app envia para
/login?next=/billing. - Após o login, seu callback lê
next(oureturnUrl,redirect,continue) e envia o usuário para lá.
Se o callback aceita next=https://evil.example, você construiu um redirecionamento aberto na parte mais confiável do seu produto.
O impacto não se limita ao phishing. Redirecionamentos dentro de fluxos OAuth também podem aumentar a área de ataque quando times passam valores sensíveis por URLs durante builds iniciais. Mesmo quando você "apenas" encaminha um code do OAuth, pode vazar dados através do histórico do navegador, logs, cabeçalhos Referer, ou simplesmente ao levar o usuário a uma página que o engana para entregar acesso.
Padrões de redirecionamento arriscados comuns para procurar
A maioria dos bugs de redirecionamento em auth começa do mesmo jeito: um time quer "retornar para onde você estava", então passam um parâmetro de redirecionamento por aí e o tratam como seguro.
Sinais de alerta para buscar:
- Qualquer parâmetro de query que controla a navegação pós-login (
next,returnTo,redirect,url) sendo usado diretamente em um redirect HTTP ouwindow.location. - Código que aceita URLs completas (
https://example.com) em vez de caminhos internos (/dashboard). - Alvos de redirecionamento vindos de localStorage, cookies ou headers sendo tratados como confiáveis.
- "Validação" que só checa
startsWith('/'). - Múltiplas etapas de decodificação/normalização que deixam confuso o que foi validado vs o que foi usado.
Dois casos de borda pegam muitos times:
Protocol-relative URLs: valores como //evil.com parecem um caminho, mas os navegadores os tratam como "use o esquema atual e vá para evil.com." Um simples startsWith('/') deixa isso passar.
URLs codificadas: atacantes podem esconder o mesmo truque na codificação. %2F%2Fevil.com vira //evil.com após decodificar. Se você valida antes de decodificar, ou decodifica mais de uma vez em lugares diferentes, pode aprovar uma string e redirecionar para outra.
Como atacantes realmente abusam de redirecionamentos abertos
Atacantes gostam de redirecionamentos abertos porque podem "emprestar" a confiança do seu domínio. A vítima vê seu site real na barra de endereço, autentica e só é enviada a algo malicioso no final.
Um ataque muito comum:
-
O atacante compartilha um link para seu domínio real que inclui um parâmetro de redirecionamento, por exemplo
?next=https://evil.example. -
Seu app mostra a página de login real.
-
Após o usuário autenticar, seu app redireciona para o site do atacante.
-
O site do atacante mostra um aviso crível de "sessão expirada" ou "confirme sua conta" e captura credenciais ou códigos MFA.
O OAuth pode piorar isso se seu endpoint de callback troca ou manipula códigos/tokens e em seguida redireciona imediatamente com base em entrada controlada pelo usuário. Mesmo que os dados sejam de curta duração, uma janela curta pode ser suficiente.
Um exemplo realista: o link de login que envia usuários para fora
Um protótipo frequentemente adiciona um parâmetro returnTo para que o login pareça polido.
Uma URL normal pode ser:
/login?returnTo=/billing
O bug aparece quando returnTo é tratado como "qualquer URL" em vez de "um caminho seguro dentro do nosso app."
Agora isto também funciona:
/login?returnTo=https://attacker.example/fake-dashboard
Nada quebra. O usuário entra com sucesso e então vai parar em um site que parece seu produto, mas não é. Do ponto de vista do usuário, seu login funcionou, então a próxima tela parece confiável.
A lição é simples: a funcionalidade "retornar para onde você estava" deve aceitar apenas destinos seguros e esperados. Se pode apontar para uma URL externa, é uma porta aberta.
O modelo mais seguro: caminhos relativos mais allowlists estritas
A abordagem mais segura é intencionalmente chata: trate o destino pós-login como um caminho interno, não como uma URL completa.
Regra 1: aceite caminhos relativos, não URLs completas
Aceite apenas valores como /settings ou /billing. Evite aceitar https://... e rejeite explicitamente valores protocol-relative como //....
Uma linha de base útil é: exigir uma única / inicial e rejeitar qualquer coisa que comece com //.
Regra 2: valide contra uma allowlist estrita
Mesmo se aceitar apenas caminhos relativos, talvez você queira restringir onde os usuários podem chegar após o auth. Uma allowlist evita destinos embaraçosos ou arriscados como loops de /logout, rotas que disparam ações sensíveis, ou páginas que só alguns papéis deveriam ver.
Mantenha a lista pequena. Permita um punhado de rotas conhecidas como seguras (ou alguns prefixos seguros) e, por padrão, direcione todo o resto para uma página segura como /dashboard.
Normalize e parse antes de decidir
Normalize a entrada uma vez: apare espaços e decodifique percent-encoding uma única vez. Depois valide o caminho resultante. Evite decodificar duas vezes ou validar em uma representação diferente daquela usada no redirecionamento.
Faça as falhas serem monótonas
Se o valor estiver ausente ou inválido, ignore-o e redirecione para um destino conhecido e seguro. Registre rejeições para detectar sondagens e código cliente quebrado.
Passo a passo: corrigindo o tratamento de redirecionamento em um protótipo
A lógica de redirecionamento tende a se espalhar por middleware, handlers de callback e código de UI. A forma mais rápida de torná-la segura é tratar todo destino de redirecionamento como entrada não confiável e centralizar a validação.
-
Faça um inventário de todas as fontes de redirecionamento: parâmetros de query (
next,returnTo,redirect,callback,continue), cookies, localStorage e qualquer middleware de auth que "lembre" para onde o usuário ia. -
Escolha sua regra: para a maioria dos apps, aceite apenas caminhos relativos. Se você realmente precisa de redirecionamentos externos (raro), permita apenas uma curta lista de origens exatas que você controla.
-
Normalize uma vez: trim, decodifique uma vez e rejeite caracteres de controle.
-
Valide estritamente:
- Exija uma
/inicial única. - Rejeite
//, qualquer esquema comohttp:oujavascript:, e barras invertidas (\\\\) incluindo barras invertidas codificadas. - Rejeite travessia como
..e bytes nulos. - Se permitir URLs completas, exija que a origem bata exatamente com sua allowlist.
- Redirecione e registre: em caso de falha, envie o usuário a um padrão seguro e registre o valor rejeitado.
Erros comuns que mantêm a vulnerabilidade viva
A maioria das correções falhas parecem "validadas" mas ainda tratam redirecionamentos como strings simples.
Armadilhas comuns:
- Allowlist por substring (por exemplo, checar
includes('mydomain.com')). Atacantes podem usarmydomain.com.evil.comou esconder texto confiável no path/query. - Validar apenas no cliente. Verificações do lado do cliente ajudam a UX, mas o servidor deve ser o portão final.
- Validar um parâmetro e redirecionar com outro por causa de helpers do framework ou precedência de parâmetros.
- Normalizar de forma inconsistente, validar antes de decodificar ou decodificar múltiplas vezes.
Também fique atento a "configuramos antes, então é confiável." Se um valor está em localStorage, um campo escondido ou um cookie, o atacante ainda pode editá-lo ou burlar a página que o definiu.
Verificações rápidas que você pode fazer antes de lançar
Você pode detectar a maioria dos problemas de redirecionamento com alguns testes focados. O objetivo é simples: nenhum valor controlado pelo usuário deve mandar o navegador para um domínio inesperado, e valores desconhecidos devem aterrissar em algum lugar seguro.
Teste o parâmetro que seu app usa após o login (next, redirect, returnTo, callbackUrl). Confirme que um caminho interno normal funciona, então tente entradas que frequentemente escapam de checagens ingênuas:
https://example.com(deve ser rejeitado)//evil.come%2F%2Fevil.com(devem ser rejeitados)\\\\evil.com(alguns frameworks normalizam isso de maneiras surpreendentes)- Uma rota interna desconhecida como
/definitely-not-real(deve cair em um padrão seguro)
Repita os mesmos testes no código de roteamento do cliente e nos endpoints do servidor que finalizam sessões ou tratam callbacks OAuth. Atacantes usarão o caminho mais fraco.
Próximos passos: chegar a uma configuração de redirecionamento limpa e segura
Bugs de redirecionamento raramente vivem em só um lugar. Em protótipos, aparecem em qualquer ponto em que o app tenta ser útil após o login: guards de rota, middleware que redireciona usuários não autenticados, handlers de callback OAuth, links de convite e fluxos de onboarding.
Um bom estado final é chato: todo redirecionamento é ou um caminho relativo conhecido e seguro, ou (se você realmente precisar) uma URL absoluta que bata exatamente com uma allowlist curta que você controla. Todo o resto é ignorado e substituído por um padrão seguro.
Se você está lidando com uma base de código gerada por IA e quer uma segunda opinião, FixMyMess (fixmymess.ai) foca em diagnosticar e reparar esse tipo de problema de autenticação e redirecionamento, além de problemas relacionados como segredos expostos e padrões inseguros que funcionam em demos mas falham em produção.