● CWE-22 · CVE-2007-4559

Travessia de caminho no tarfile.extractall do Python (CVE-2007-4559): como acontece e como corrigir

Antes dos filtros de extração, tarfile.extractall() gravava cada membro no caminho guardado no arquivo compactado, então nomes com .., caminhos absolutos ou links podiam pôr arquivos fora da pasta de destino. Como o argumento filter="data" da PEP 706 bloqueia isso, por que o destino não pode vir da entrada do usuário e quais limites de tamanho adicionar, com um exemplo em Python.

Explicação em Linguagem Simples (ELI5)

Uma empresa de mudanças desembala caixas no seu novo apartamento e coloca cada item onde a etiqueta mandar. Uma etiqueta que diz 'cozinha, depois pela porta da frente, depois no quarto do vizinho' é seguida ao pé da letra, porque os carregadores só leem etiquetas e nunca conferem se o endereço ainda fica dentro do seu apartamento. Um arquivo compactado funciona igual: cada arquivo traz o próprio caminho de destino. A correção é um encarregado que lê cada etiqueta antes e recusa tudo o que sairia do apartamento, além de qualquer coisa que não seja uma caixa comum, como uma placa apontando para outro lugar.

Conceitos Centrais e Termos

CVE-2007-4559
O módulo tarfile do Python não impedia que nomes de membros saíssem do diretório de extração. O problema era conhecido desde 2007, e em 2022 a Trellix estimou que cerca de 350 mil projetos de código aberto chamavam extractall() em arquivos não confiáveis.
Nomes de membro com .. ou caminhos absolutos
Uma entrada tar chamada ../outside.txt ou /etc/cron.d/job resolve para fora do destino quando é juntada a ele. Sem filtro, extractall() grava nesse caminho resolvido com as permissões do processo.
Membros que são links simbólicos ou físicos
Um arquivo compactado pode primeiro criar dentro do destino um link que aponta para outro lugar e depois gravar um arquivo comum através dele. Verificar só os nomes não pega isso, e por isso os filtros também inspecionam o alvo dos links.
Filtros de extração (PEP 706)
filter="data" rejeita nomes absolutos, escapes com .., links que saem do destino e arquivos de dispositivo, e remove os bits set-user-ID e de execução para outros. Ele existe a partir do Python 3.12 e nas versões de segurança do 3.8 ao 3.11, e passa a ser o padrão no Python 3.14.
Limites de recursos
Os filtros não limitam tamanho. Um arquivo compactado pequeno pode se expandir em milhões de membros ou muitos gigabytes, então conte os membros e o tamanho total antes de extrair.

Fluxo de Ataque Passo a Paso

Passo 1

O app aceita um arquivo compactado enviado

Uma importação de dados, instalador de plugins ou manipulador de artefatos de CI recebe um .tar.gz de um usuário.

Passo 2

Um nome de membro aponta para fora da pasta

O caminho guardado de uma entrada sobe para fora do destino ou é absoluto. Um arquivo de teste com um membro chamado ../outside.txt ilustra o caso.

Passo 3

O extractall grava no caminho resolvido

O arquivo vai parar onde o nome resolver, e o próprio destino pode vir de um job_id enviado pelo usuário.

Passo 4

Arquivos da aplicação são substituídos

Configurações, templates ou scripts sobrescritos rodam na próxima vez que a aplicação os carregar, e é assim que essa travessia de caminho vira execução de código.

Código-Fonte: Vulnerável vs. Seguro

IMPLEMENTAÇÃO VULNERÁVEL
# imports.py: cada membro vai para onde o nome guardado apontar
import tarfile
from pathlib import Path

UPLOAD_ROOT = Path("/srv/app/imports")


# pasta escolhida por quem chama, sem filtro, sem limites de tamanho
def import_bundle(archive_path: str, job_id: str) -> Path:
    dest = UPLOAD_ROOT / job_id
    dest.mkdir(parents=True, exist_ok=True)
    with tarfile.open(archive_path) as tar:
        tar.extractall(dest)
    return dest
PATCH SEGURO E ROBUSTO
# imports.py: filtro data, pasta com nome do servidor e limites de tamanho
import tarfile
import uuid
from pathlib import Path

UPLOAD_ROOT = Path("/srv/app/imports")
MAX_MEMBERS = 10_000
MAX_TOTAL_BYTES = 500 * 1024 * 1024


# rejeita arquivos com membros ou bytes demais
def check_limits(tar: tarfile.TarFile) -> None:
    members = tar.getmembers()
    if len(members) > MAX_MEMBERS or sum(m.size for m in members) > MAX_TOTAL_BYTES:
        raise ValueError("archive too large")


# filter='data' bloqueia .., nomes absolutos, links para fora e arquivos de dispositivo
def import_bundle(archive_path: str) -> Path:
    dest = UPLOAD_ROOT / uuid.uuid4().hex
    dest.mkdir(parents=True)
    with tarfile.open(archive_path, mode="r:*") as tar:
        check_limits(tar)
        tar.extractall(dest, filter="data")
    return dest

Lista de Verificação de Segurança para Engenharia

Fontes

← Ver o diretório completo de segurança Todos os guias de vulnerabilidades →