Executando verificação de segurança...
44

[TabNews – Postmortem] Open Redirect descoberto na página de login

Na última sexta-feira, fomos informados pelo Renan sobre uma vulnerabilidade do tipo Open Redirect em nossa página de login. 😱 O problema já foi corrigido ontem. 🙌

Embora a falha pudesse ser explorada em determinados cenários, não encontramos nenhum indício de abuso.

O que aconteceu

A página de login do TabNews aceita um parâmetro redirect para redirecionar o usuário de volta para a página que estava antes do login. A ideia é que, ao tentar interagir com uma publicação sem estar autenticado, ele seja levado para a página de login e, após autenticar, volte para a rota original.

O problema é que estávamos validando o redirect apenas verificando se ele começava com / sem começar com //, que poderia indicar um domínio externo (por exemplo, //curso.dev).

No entanto, como o Renan alertou, ainda era possível burlar essa verificação com uma URL como esta:

https://tabnews.com.br/login?redirect=/%09/curso.dev

O %09 é só um exemplo, e representa uma tabulação. O que acontecia:

  1. O Next.js automaticamente convertia %09 em uma tabulação real (\t) ao preencher router.query.redirect.
  2. O caminho era recebido como "/\t/curso.dev" e considerado como interno.
  3. Ao usar router.replace(redirect), o caractere de tabulação era descartado silenciosamente, transformando o caminho em //curso.dev.
  4. Isso resultava em um redirecionamento para https://curso.dev, o que nunca deveria ser possível, já que o redirecionamento deve ocorrer apenas para rotas internas do TabNews.

Como corrigimos

A correção foi feita garantindo que o valor passado no redirect seja interpretado de forma segura:

  • Em vez de confiar na string bruta, agora construímos um new URL(redirect, location.origin) e validamos se url.origin continua sendo igual a location.origin.
  • Só redirecionamos se essa condição for verdadeira. Isso impede que qualquer caminho externo (mesmo malformado ou disfarçado) seja aceito.

Com essa abordagem, mesmo que o redirect contenha espaços, quebras de linha ou tabulações, o redirecionamento só ocorre se o destino final continuar dentro do domínio do TabNews.

O que aprendemos

Esse incidente reforça que:

  • Verificações simples, como "começa com / e não com //", não são suficientes quando múltiplos componentes (navegador, framework, bibliotecas) participam do parsing da URL.
  • A decodificação automática feita pelo Next.js nos parâmetros da URL pode introduzir riscos se não forem considerados os efeitos colaterais, como a remoção silenciosa de certos caracteres.
  • A validação robusta de origem com new URL(..., base) é uma abordagem muito mais segura do que tentar manipular strings manualmente.

O que ainda podemos melhorar

Seria ótimo adicionar testes automatizados específicos para validar esse tipo de redirecionamento e garantir que o comportamento continue seguro no futuro.

Como reportar vulnerabilidades

Se você encontrar qualquer problema de segurança no TabNews, pedimos que utilize uma das opções abaixo para fazer o reporte:

Todos os relatos são analisados com prioridade, e agradecemos desde já qualquer colaboração nesse sentido.

Agradecimentos

Nosso agradecimento ao Renan, que identificou e compartilhou a vulnerabilidade de forma ética. 💪 Esse tipo de contribuição ajuda a tornar o TabNews mais seguro para todo mundo. 🚀

Carregando publicação patrocinada...
39

Estou muito feliz em contribuir com a comunidade 🤗🚀

Acrescentando ao post, vou tentar explicar de forma mais detalhada o que acontece por debaixo dos panos.

Detalhamento

Como podemos ver no trecho de código abaixo, o router.replace do Next.js utiliza a API padrão URL() para obter o caminho:

...
replace: (href: string, options?: NavigateOptions) => {
    startTransition(() => {
      dispatchNavigateAction(href, 'replace', options?.scroll ?? true, null)
    })
},
... 

export function dispatchNavigateAction(
  href: string,
  navigateType: NavigateAction['navigateType'],
  shouldScroll: boolean,
  linkInstanceRef: LinkInstance | null
): void {
  const url = new URL(addBasePath(href), location.href) //<<<
...

Fonte: next.js no GitHub

Ao enviar caracteres de controle (\n, \t e outros) ao parâmetro do new URL(), eles serão descartados e no final traduzidos para uma URL de protocolo relativo, com duas //:

Isso: "/\n/curso.dev"
Vira isso: "//curso.dev"
E o objeto vem assim:
new URL("/\n/curso.dev", "http://tabnews.com.br")

hash: "",
host: "curso.dev",
hostname: "curso.dev",
href: "http://curso.dev/",
origin: "http://curso.dev",
password: "",
pathname: "/",
port: "",
protocol: "http:",
search: "",
searchParams: URLSearchParams {size: 0},
username: ""

O que no início era para ser uma URL relativa, agora se tornou uma URL externa. Isso acontece pois a URL API segue o padrão WHATWG URL Standard, que especifica:

"Preprocessing | Before parsing, the input string input must be preprocessed:

  1. Remove all leading and trailing c0-space characters from input.
  2. Remove all u+9 (tab), u+A (line-feed) and u+D (carriage-return) characters from input."

Fonte: URL Specification

Portanto, as verificações simples presentes no mecanismo de redirect não esperavam por esse comportamento, abrindo margem para ataques. Vulnerabilidades open redirect tendem a ser comuns, já que geralmente passam despercebidas por nós devs.

Como eu encontrei a vulnerabilidade?

Para ser sincero, eu já tinha visto há um tempo esse mecanismo de redirecionamento no login, mas não consegui quebrar. Até que um certo dia, resolvi dar uma maior atenção a essa possível vulnerabilidade, e pesquisando encontrei esse "truque" para burlar as condicionais.

Vale dizer que não me considero um hacker ético, muito menos um especialista em Segurança da Informação. Atualmente, sou apenas um Desenvolvedor Web Júnior, como muitos por aí. Isso mostra que qualquer pessoa, com curiosidade e responsabilidade, pode buscar vulnerabilidades e reportá-las de forma ética.

Espero que o conteúdo tenha ficado claro. E muito obrigado pelas menções na publicação! 🤝