Uma raiz publicada deveria funcionar como uma fronteira: o cliente autorizado a acessar backups/equipe-a não deveria alcançar backups/equipe-b, mesmo quando a credencial usada pelo processo de armazenamento possui acesso a ambas. A CVE-2026-71309 quebrava exatamente essa expectativa no servidor REST compatível com restic oferecido pelo rclone.
Em versões afetadas, um caminho de URL iniciado por ../ podia atravessar a raiz configurada pelo operador. Dependendo do backend, uma requisição enviada ao endpoint REST permitia ler, criar, sobrescrever ou excluir objetos irmãos e objetos localizados em diretórios pais. O defeito estava na validação comum do servidor, mas a exploração concreta dependia de como cada backend combinava a raiz com o caminho recebido.
O problema foi classificado como CWE-22, recebeu severidade alta e pontuação CVSS v4.0 de 8,6. Todas as versões do rclone de 1.40.0 até 1.74.4 são consideradas afetadas; a correção está disponível na versão 1.75.0. Essas informações constam no advisory oficial GHSA-45pq-889g-fcgh.
A exploração exige que o endpoint REST esteja alcançável, que o operador tenha publicado uma subpasta de um backend e que a credencial desse backend possa acessar algum objeto fora da subpasta. O atacante também precisa conseguir fazer requisições ao servidor; no vetor CVSS publicado, isso corresponde a privilégios baixos. Não é necessária interação de outro usuário, condição de corrida ou conhecimento de uma característica imprevisível do alvo.
O ponto mais importante é que a vulnerabilidade não concede ao processo rclone permissões novas no backend. Ela permite que um cliente ultrapasse a fronteira lógica que o operador tentou criar ao publicar apenas uma subpasta. O alcance final continua limitado pelas permissões da credencial do backend e pela semântica de caminhos daquele armazenamento.
O que o rclone serve restic publica
O comando rclone serve restic expõe uma API REST compatível com o protocolo usado pelo restic sobre um filesystem do rclone. O filesystem pode apontar para um backend inteiro ou para uma subpasta. Por exemplo:
rclone serve restic webdav:backups/equipe-a
Nesse cenário, backups/equipe-a é a raiz apresentada pelo serviço. Mesmo que a credencial WebDAV consiga enxergar backups/equipe-b, o cliente da API deveria ficar confinado à raiz publicada. O servidor precisa garantir essa propriedade antes de transformar um caminho de URL em um nome de objeto do backend.
Essa separação é especialmente importante quando uma mesma conta de armazenamento atende vários repositórios, clientes ou jobs de automação. A autenticação do endpoint responde à pergunta “quem pode chamar a API?”, mas a raiz publicada responde a outra pergunta: “quais objetos esse cliente pode alcançar?”. Autenticar o cliente não compensa uma falha na segunda fronteira.
Causa raiz: canonicalização não é validação de contenção
O componente vulnerável era o middleware backend-independente WithRemote, localizado em cmd/serve/restic/restic.go. Ele obtinha o caminho da requisição, removia barras externas e tentava rejeitar caminhos não canônicos comparando a entrada com o resultado de path.Clean:
urlpath = strings.Trim(urlpath, "/")
if urlpath != "" && path.Clean(urlpath) != urlpath {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
A intenção parecia razoável: se a limpeza alterasse o texto, o caminho continha alguma construção redundante ou potencialmente perigosa. Entretanto, path.Clean normaliza um caminho; ele não certifica que um caminho relativo permanece abaixo de uma raiz futura.
Considere estes resultados:
path.Clean("../outside.txt") = "../outside.txt"
path.Clean("../../outside.txt") = "../../outside.txt"
path.Clean("a/../../outside.txt") = "../outside.txt"
Nos dois primeiros casos, o componente pai já está no início do caminho relativo. A função não tem um diretório anterior que possa remover e, corretamente para sua finalidade de limpeza, preserva ... Como entrada e saída são iguais, a condição vulnerável aceita a requisição.
No terceiro caso, a limpeza elimina a/.. e altera a string. A comparação detecta a diferença e retorna HTTP 400. Esse comportamento parcial torna o defeito menos óbvio: testes com a/../../arquivo parecem confirmar que a proteção funciona, enquanto a forma mais direta ../arquivo passa pela mesma verificação.
O erro conceitual é usar igualdade após canonicalização como substituto de uma regra de validade. Um caminho canônico ainda pode ser proibido. ../outside.txt é uma representação canônica de um caminho relativo que sobe um nível, mas não é um nome aceitável quando a política exige permanência abaixo da raiz publicada.
Da URL ao backend
Após aceitar o caminho, WithRemote o armazenava no contexto da requisição:
ctx := context.WithValue(r.Context(), ContextRemoteKey, urlpath)
next.ServeHTTP(w, r.WithContext(ctx))
Os handlers seguintes confiavam nesse valor. As operações de leitura resolviam o remoto por meio de NewObject; uploads enviados por POST chegavam a operations.RcatSize; exclusões resolviam o objeto e chamavam Remove. Não havia uma segunda verificação comum de contenção antes de entregar o nome ao backend.
Isso produzia uma cadeia simples:
URL com %2e%2e/arquivo
↓ decodificação
../arquivo
↓ WithRemote aceita e armazena no contexto
handler GET, HEAD, POST ou DELETE
↓
backend combina sua raiz com ../arquivo
↓
operação potencial fora da raiz publicada
A codificação %2e%2e não é uma variante independente da falha. Ela representa .. na URL e permite observar o comportamento depois que a camada HTTP decodifica o caminho. Ferramentas e proxies podem normalizar caminhos antes do envio, razão pela qual uma reprodução controlada precisa preservar a forma original da URL.
Por que o resultado muda conforme o backend
A validação defeituosa estava na camada comum do servidor, mas os backends não tratavam o remoto da mesma forma. Alguns combinavam a raiz e o caminho com path.Join antes de codificar o nome para o protocolo de armazenamento. Nesses casos, .. removia a subpasta publicada. Outros codificavam componentes especiais como caracteres de nome de arquivo, impedindo a fuga na configuração testada.
No WebDAV, por exemplo, a construção relevante era equivalente a:
subPath := path.Join(f.root, file)
Com os valores abaixo:
f.root = "served-root"
file = "../outside-secret.txt"
path.Join("served-root", "../outside-secret.txt")
= "outside-secret.txt"
Quando a requisição finalmente chegava ao servidor WebDAV, ela já parecia uma operação normal sobre outside-secret.txt. O backend remoto não sabia que o rclone pretendia limitar o cliente a served-root.
Os testes publicados confirmaram leitura, escrita e exclusão fora da raiz com WebDAV, FTP, Memory e SFTP. No backend HTTP, que é somente leitura, foi confirmada a leitura externa. Em todos esses casos, o caminho inseguro aceito pelo servidor chegou a uma implementação que preservava ou resolvia o componente pai de forma capaz de ultrapassar a subpasta publicada.
Nos testes com um backend S3 compatível e com o backend local, a fuga não aconteceu. Essas implementações codificaram .. como parte do nome do objeto, em vez de interpretá-lo como navegação para o diretório pai.
“Nenhuma fuga observada” não significa que um backend recebeu uma garantia universal de segurança para todas as configurações, versões e modos de codificação. Significa apenas que a técnica analisada não ultrapassou a raiz nos testes publicados. Da mesma forma, a existência do defeito em WithRemote não prova automaticamente exploração contra todos os backends suportados pelo rclone.
Pré-condições e cenários de impacto
O risco prático aparece quando o serviço executa uma versão anterior à 1.75.0, publica uma subpasta sem isolar a credencial exatamente nessa raiz, aceita requisições de um cliente potencialmente não confiável e usa um backend cuja semântica resolve o caminho de modo vulnerável. A credencial também precisa possuir permissão sobre objetos pais ou irmãos; a falha não amplia as permissões concedidas pelo armazenamento.
Uma estrutura multiusuário ilustra o problema:
backups/
├── cliente-a/
├── cliente-b/
└── configuracao-de-restauracao.json
Se o serviço publica apenas backups/cliente-a, uma requisição para ../cliente-b/... pode atravessar a separação entre clientes. Uma leitura compromete a confidencialidade de snapshots, índices ou metadados. Um POST pode criar ou sobrescrever um objeto que outro job consome. Um DELETE pode remover objetos externos quando a credencial e o backend permitem a operação.
O impacto não precisa terminar no armazenamento. Um arquivo sobrescrito pode ser posteriormente interpretado como configuração, script, manifesto ou artefato de restauração por outro sistema. Esse encadeamento é contextual: a CVE permite alterar o objeto; a execução ou confiança posterior depende do ambiente.
A opção --append-only reduz parte do risco de sobrescrita e exclusão, mas não corrige a validação. O advisory registra que leituras por travessia e criação de objetos externos ainda podem ocorrer, além de cenários específicos de exclusão relacionados a caminhos de locks. Ela deve ser vista como redução parcial de impacto, não como remediação.
Como a exploração se manifestava
Em uma instalação vulnerável, a requisição podia preservar o componente pai na URL usando sua forma codificada. Um acesso equivalente ao exemplo abaixo tentava buscar outside-secret.txt, que estava um nível acima da raiz publicada:
GET /%2e%2e/outside-secret.txt HTTP/1.1
Host: restic.example
Authorization: Basic …
Depois da decodificação, o servidor trabalhava com ../outside-secret.txt. No WebDAV, a combinação com served-root resultava em outside-secret.txt; por isso o backend recebia uma leitura comum fora da subpasta pretendida. A mesma ideia aplicada a POST ou DELETE podia criar, sobrescrever ou excluir objetos quando o backend e a credencial permitiam essas operações.
O comportamento correto é rejeitar o caminho com HTTP 400 antes de chamar o backend. A diferença entre a versão vulnerável e a corrigida não está na aparência final da operação de armazenamento, mas no ponto em que o caminho deixa de ser aceito como um nome válido.
Como a correção funciona
A correção está na versão 1.75.0 do rclone. Ela substituiu a comparação com path.Clean por uma regra explícita de validade baseada em io/fs.ValidPath. Em vez de perguntar se a limpeza alterou o texto, o servidor passa a perguntar se o caminho é um nome relativo válido antes que ele chegue a qualquer backend.
ValidPath aceita . como a representação especial da raiz de um filesystem. Para os demais valores, exige um caminho relativo sem raiz, codificado em UTF-8 válido e sem componentes vazios, . ou ... O servidor trata suas duas particularidades explicitamente: mantém o caminho vazio como a raiz legítima da API e rejeita . como nome de objeto.
A mudança corrige o problema no limite comum, antes que qualquer backend veja o remoto. Essa localização é importante: tentar compensar em cada backend deixaria espaço para divergência, novas implementações vulneráveis e operações esquecidas.
A release também adicionou testes de regressão para GET, HEAD, POST e DELETE, exercitando entradas como .., ../, ../../, ../outside-secret.txt, %2e%2e/outside-secret.txt, travessias internas, . e ./inside.txt contra um backend que resolve componentes pais ao combinar caminhos, e verificando tanto o HTTP 400 quanto a integridade dos objetos externos.
Remediação e defesa em profundidade
A correção primária é atualizar para o rclone 1.75.0 ou posterior. Reinicie os processos e verifique a versão efetivamente carregada. Imagens imutáveis, hosts com múltiplas instalações e serviços que mantêm um binário antigo em memória podem fazer uma atualização aparentemente concluída não chegar à instância ativa.
Quando a atualização imediata não for possível, a exposição pode ser reduzida restringindo o endpoint às redes, identidades e jobs estritamente necessários. A credencial do backend deve começar exatamente na raiz publicada, e clientes ou repositórios distintos devem usar compartilhamentos, buckets ou contas separados. Permissões de escrita e exclusão também devem ser removidas quando o fluxo precisar apenas de leitura.
Essas medidas limitam o alcance possível, mas não corrigem a validação. Autenticação, proxy reverso e --append-only não devem ser tratados como substitutos da atualização. Logs de caminhos anômalos e operações em objetos irmãos podem ajudar na investigação, embora a ausência desses registros não prove que nenhuma exploração ocorreu.
Um Web Application Firewall pode rejeitar padrões conhecidos em uma emergência, mas normalização divergente entre proxy e aplicação torna esse tipo de regra frágil. O controle definitivo deve existir no componente que interpreta o caminho, e ele já está disponível na versão corrigida.
Conclusão
A CVE-2026-71309 mostra uma diferença fundamental entre limpar um caminho e validar uma fronteira. path.Clean produzia uma forma canônica, mas preservava componentes pais iniciais; a comparação de strings interpretava essa estabilidade como segurança. Depois que o valor era aceito, handlers e backends o tratavam como confiável, e algumas implementações removiam a raiz publicada ao combinar os caminhos.
O resultado podia quebrar isolamento entre subpastas e permitir leitura ou alteração de objetos externos dentro do alcance da credencial do backend. A correção centraliza uma regra de validade explícita com io/fs.ValidPath e cobre os principais métodos e variantes em testes de regressão.
Operadores devem atualizar para a versão 1.75.0 ou posterior, verificar o binário realmente em execução e revisar se as credenciais de armazenamento respeitam o mesmo limite que a API pretende publicar. Isolamento de credenciais e menor privilégio continuam valiosos, mas complementam a correção; não a substituem.