m4z4r0p3, do nosso grupo de pesquisas de CVEs da Sunsec, me apontou um problema: ao testar o exploit do artigo anterior, ele notou que o payload não se comportava da mesma forma em todos os browsers. No Chrome e Firefox, a navegação para o host malicioso não acontecia. No Safari, sim.
Fomos investigar a fundo e encontramos os pontos exatos nos repositórios open source onde isso é implementado.
O ponto de partida
O artigo anterior mostrou que o Express envia o header:
Location: http://google.com%5C@evil.com
A partir deste momento, cada browser tem um pipeline diferente para processar esse header. Mas há algo que todos fazem igual antes de divergir.
O que é igual em todos os browsers
Todos os browsers decodificam %5C → \. Ficam com:
http://google.com\@evil.com
É a partir daqui que os caminhos divergem.
Chrome e Firefox: normalização antes de interpretar
Chrome e Firefox implementam o WHATWG URL Standard de forma muito rigorosa no processamento de redirects. O pipeline deles é:
- Recebem
http://google.com\@evil.com - Normalizam
\→/durante o parse, antes de interpretar a estrutura - Ficam com
http://google.com/@evil.com - Agora a estrutura é clara:
google.comé o host,@evil.comé parte do path - Navegam para google.com ✅
O ponto-chave: eles normalizam no momento em que processam o header Location, antes de decidir quem é o host.
Safari: interpretação antes de normalizar
O Safari também implementa WHATWG, mas com uma diferença sutil na ordem em que aplica as normalizações durante redirects:
- Recebe
http://google.com\@evil.com - Interpreta a estrutura antes de normalizar completamente o
\ - Nesse momento,
\ainda não foi tratado como/ - Vê
google.comcomo userinfo (antes do@) eevil.comcomo host - Normaliza o
\→/depois — mas o host já foi decidido - Navega para evil.com ☠️
| Browser | Ordem das operações | Host decidido |
|---|---|---|
| Chrome/Firefox | Normaliza \ → / → interpreta estrutura | google.com |
| Safari | Interpreta estrutura → normaliza \ | evil.com |
Nenhum está “errado”, a spec não cobre esse caso de borda explicitamente para o fluxo de redirect HTTP. O Safari é simplesmente menos preventivo na sanitização.
Evidência no código-fonte
Encontramos os pontos exatos onde isso é implementado nos repositórios open source do Chrome e do Firefox.
Chromium — url/url_parse_internal.h
O Chromium define a função IsSlashOrBackslash() com um comentário direto sobre a motivação: é a própria URL Standard que diz tratar \ como / em URLs especiais.
// A helper function to handle a URL separator, which is '/' or '\'.
//
// The motivation: There are many condition checks in URL Standard like the
// following:
//
// > If url is special and c is U+002F (/) or U+005C (\), ...
inline bool IsSlashOrBackslash(char16_t ch) {
return ch == '/' || ch == '\\';
}
Arquivo: url/url_parse_internal.h
Essa função é chamada durante o parse, antes de qualquer decisão sobre o host. O \ entra como equivalente a / desde o início do processamento.
Chromium — url/url_canon_host.cc
No arquivo url_canon_host.cc, existe uma tabela kHostCharacterTable onde \ é marcado como kForbiddenHost, um forbidden host code point segundo a spec WHATWG:
kForbiddenHost, // '\\' ← \ é forbidden host code point
Arquivo: url/url_canon_host.cc
Quando o Chromium encontra \ durante o parse do host, ele rejeita o caractere imediatamente. A URL google.com\@evil.com nunca chega a ser interpretada com evil.com como host.
Firefox — rust-url/url/src/parser.rs
O Firefox usa a crate servo/rust-url como seu parser WHATWG. Ela define explicitamente que \ é tratado como separador de path em URLs especiais:
// The backslash (\) character is treated as a path separator in special URLs
// so it needs to be additionally escaped in that case.
pub(crate) const SPECIAL_PATH_SEGMENT: &AsciiSet = &PATH_SEGMENT.add(b'\\');
E na função parse_path_start():
if scheme_type.is_special() {
if maybe_c == Some('\\') {
// If c is U+005C (\), validation error.
self.log_violation(SyntaxViolation::Backslash);
}
if !self.serialization.ends_with('/') {
self.serialization.push('/');
if maybe_c == Some('/') || maybe_c == Some('\\') {
return self.parse_path(...);
}
}
}
Arquivo: url/src/parser.rs — servo/rust-url
O \ é normalizado para / durante o parse do path, antes que qualquer lógica de host possa ser confundida.
Por que isso é difícil de perceber
O artigo anterior mostrou que o new URL() do Node.js (que segue WHATWG) já resolve corretamente para evil.com:
new URL('http://google.com\\@evil.com').hostname
// → 'evil.com'
Ou seja, o parser WHATWG puro concorda com o Safari. Chrome e Firefox fazem uma etapa extra de sanitização especificamente no fluxo de redirect HTTP, um comportamento defensivo adicional, não parte explícita da spec.
O CVE-2024-29041 vai funcionar como um exploit específico para Safari, o que não diminui sua seriedade, especialmente em contextos de phishing onde o atacante pode direcionar vítimas com qualquer browser, mas é importante saber ao testar e reportar, visto que isso pode mudar e muito o risco e impacto.