
O Consul Template validava para onde o symlink apontava durante a avaliação do template, mas sua busca de dependência posterior lia o caminho original. Redirecionar o link entre essas operações transformou uma referência de arquivo dentro do sandbox em uma divulgação de arquivo fora do sandbox.
O Consul Template validava para onde um symlink apontava durante a avaliação do template, mas a busca de dependência posterior lia o caminho original. Redirecionar o link entre essas operações transformava uma referência de arquivo dentro do sandbox em uma divulgação de arquivo fora do sandbox.
Encontrei este problema enquanto revisava o HashiCorp Consul Template, com uma questão de segurança muito específica em mente:
Se o sandbox_path valida um symlink durante a avaliação do template, a leitura de arquivo posterior permanece vinculada ao mesmo alvo validado?
Neste caso, a resposta foi não.
O helper de template file resolvia o caminho e aplicava o sandbox configurado durante a avaliação do template. Mas, após essa verificação, ele criava uma dependência de arquivo usando o caminho bruto original.
Essa dependência era buscada posteriormente.
Se um atacante redirecionasse o symlink na lacuna entre a validação e a busca da dependência, o Consul Template poderia ler um arquivo fora do sandbox. Se o link fosse restaurado antes da próxima renderização, a validação do sandbox passava novamente e o conteúdo externo em cache ainda era renderizado.
Esse problema tornou-se o CVE-2026-5061.
Boletim da HashiCorp: HCSEC-2026-12
Boletim da IBM: boletim de segurança CVE-2026-5061
CVE: CVE-2026-5061
Corrigido em: 0.42.0
photo0
attacker-controlled in-sandbox symlink -> sandbox validation resolves to safe target -> dependency stores original raw path -> attacker retargets link outside sandbox -> dependency fetch reads external file -> attacker restores safe link -> next validation passes -> cached external content is rendered
Consul Template é uma ferramenta de renderização de templates para dados do Consul e do Vault.
Ele pode ser executado continuamente, monitorar dependências em busca de alterações e renderizar dados em arquivos ou variáveis de ambiente para que os aplicativos consumam.
O helper de template file lê um arquivo local e insere seu conteúdo na saída renderizada.
Como as leituras de arquivos locais podem expor segredos legíveis pelo processo, o Consul Template fornece o sandbox_path como um limite para esse helper.
A propriedade de segurança documentada é direta:
file devem estar dentro do sandbox configuradoIsso torna o sandbox_path um limite de segurança real.
A questão importante não era se o caminho parecia estar sob o sandbox.
A verdadeira questão era:
O arquivo que é lido permanece sendo o mesmo arquivo que passou pela validação do sandbox?
Nas versões vulneráveis, não permanecia.
Sandboxes de sistema de arquivos frequentemente falham na lacuna entre a validação do caminho e o acesso ao arquivo.
O padrão comum é:
Essa suposição é insegura quando um atacante pode modificar um symlink ou um redirecionamento equivalente do sistema de arquivos entre as duas operações.
O Consul Template tornou essa superfície especialmente interessante porque a avaliação do template e a busca de dependências eram estágios separados.
Essa separação criou a questão de segurança certa:
A busca da dependência está vinculada ao alvo validado, ou ela resolve novamente o caminho controlado pelo atacante?
Era esse o limite no qual eu me concentrei.
A causa raiz era uma incompatibilidade de tempo-de-verificação para tempo-de-uso entre:
template/funcs.godependency/file.goNa revisão testada, o fileFunc() fazia o seguinte:
normalized := strings.TrimSpace(s)
err := pathInSandbox(sandboxPath, normalized)
if err != nil {
return "", err
}
d, err := dep.NewFileQuery(s)
O helper de validação resolvia corretamente os symlinks antes de verificar a contenção:
sandboxResolved, err := filepath.EvalSymlinks(filepath.Clean(sandbox))
targetResolved, err := filepath.EvalSymlinks(filepath.Clean(path))
rel, err := filepath.Rel(sandboxResolved, targetResolved)
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("'%s' is outside of sandbox", path)
}
Portanto, a própria verificação do sandbox entendia o alvo resolvido.
Mas esse alvo resolvido era descartado.
O NewFileQuery() armazenava a string original em vez disso:
return &FileQuery{
stopCh: make(chan struct{}, 1),
path: s,
}, nil
Então, a busca posterior da dependência lia esse caminho novamente:
data, err := os.ReadFile(d.path)
Essa é a vulnerabilidade completa.
O código verificava uma resolução do sistema de arquivos e, depois, usava outra.
Porque um symlink é mutável.
O atacante não precisa fazer um alvo fora do sandbox passar pelo pathInSandbox() diretamente.
O atacante só precisa mudar para onde o caminho bruto já aprovado resolve, após a validação, mas antes que o Fetch() realize a leitura.
A sequência é:
pathInSandbox()FileQuery registra o caminho bruto do linkos.ReadFile(d.path) resolve o link novamenteEsse último passo é importante.
Restaurar o link seguro não removia o segredo já buscado do cache de dependências.
A distinção importante é a bypass de um controle de segurança explícito.
Isso não era meramente:
"o arquivo mudou enquanto o Consul Template o monitorava"
Monitorar arquivos em busca de alterações é um comportamento esperado.
O problema real era:
um caminho que passou pela restrição de sandbox documentada poderia, posteriormente, ser usado para ler um arquivo fora desse sandbox
Isso é uma falha direta do limite de confiança.
O aplicativo já havia tomado uma decisão de segurança:
Mas a busca posterior não estava vinculada ao alvo que justificava essa decisão.
Foi isso que transformou a mutabilidade comum do sistema de arquivos em uma vulnerabilidade.
Construí um reprodutor independente em torno da sequência exata de avaliação e busca de dependência.
A configuração controlada usava:
O fluxo de reprodução era: