Path traversal di tarfile.extractall Python (CVE-2007-4559): bagaimana terjadinya dan cara memperbaikinya
Sebelum ada filter ekstraksi, tarfile.extractall() menulis setiap anggota ke path yang tersimpan di arsip, sehingga nama dengan .., path absolut, atau tautan bisa menaruh file di luar folder tujuan. Bagaimana argumen filter="data" dari PEP 706 memblokirnya, mengapa folder tujuan tidak boleh berasal dari input pengguna, dan batas ukuran apa yang perlu ditambahkan, dengan contoh Python.
Penjelasan Sederhana (ELI5)
Jasa pindahan membongkar kardus di apartemen baru Anda dan menaruh setiap barang di tempat yang disebut labelnya. Label bertuliskan 'dapur, lalu keluar lewat pintu depan, lalu ke kamar tidur tetangga' diikuti apa adanya, karena para pekerja hanya membaca label dan tidak pernah memeriksa apakah alamatnya masih di dalam apartemen Anda. Arsip bekerja dengan cara yang sama: setiap file membawa path tujuannya sendiri. Perbaikannya adalah mandor yang membaca setiap label lebih dulu dan menolak apa pun yang akan keluar dari apartemen, termasuk apa pun yang bukan kardus biasa, misalnya papan penunjuk ke tempat lain.
Konsep Kunci & Istilah
- CVE-2007-4559
- Modul
tarfilePython tidak mencegah nama anggota keluar dari direktori ekstraksi. Masalah ini sudah dikenal sejak 2007, dan pada 2022 Trellix memperkirakan sekitar 350.000 proyek open-source memanggilextractall()pada arsip yang tidak tepercaya. - Nama anggota dengan .. atau path absolut
- Entri tar bernama
../outside.txtatau/etc/cron.d/jobmengarah ke luar tujuan setelah digabungkan dengannya. Tanpa filter,extractall()menulis ke path itu dengan izin proses. - Anggota berupa symbolic link dan hard link
- Arsip bisa lebih dulu membuat tautan di dalam folder tujuan yang menunjuk ke tempat lain, lalu menulis file biasa melaluinya. Memeriksa nama saja tidak menangkapnya, karena itu filter juga memeriksa target tautan.
- Filter ekstraksi (PEP 706)
filter="data"menolak nama absolut, pelarian lewat.., tautan yang keluar dari tujuan, dan file perangkat, serta menghapus bit set-user-ID dan izin eksekusi untuk pengguna lain. Filter ini tersedia sejak Python 3.12 dan di rilis keamanan 3.8 sampai 3.11, dan menjadi default di Python 3.14.- Batas sumber daya
- Filter tidak membatasi ukuran. Arsip terkompresi yang kecil bisa mengembang menjadi jutaan anggota atau banyak gigabyte, jadi hitung jumlah anggota dan ukuran total sebelum mengekstrak.
Alur Serangan Langkah demi Langkah
Aplikasi menerima arsip yang diunggah
Impor data, pemasang plugin, atau penangan artefak CI menerima .tar.gz dari pengguna.
Nama anggota menunjuk ke luar folder
Path tersimpan milik salah satu entri naik keluar dari tujuan atau berupa path absolut. Arsip uji dengan anggota bernama ../outside.txt menggambarkan kasus ini.
extractall menulis ke path hasil resolusi
File mendarat di mana pun namanya mengarah, dan folder tujuan itu sendiri bisa berasal dari job_id kiriman pengguna.
File aplikasi diganti
Konfigurasi, template, atau skrip yang tertimpa dijalankan saat aplikasi memuatnya lagi, dan beginilah path traversal ini berubah menjadi eksekusi kode.
Kode Sumber: Rentan vs Aman
# imports.py: setiap anggota ditulis ke mana pun nama tersimpannya menunjuk
import tarfile
from pathlib import Path
UPLOAD_ROOT = Path("/srv/app/imports")
# folder dipilih pemanggil, tanpa filter, tanpa batas ukuran
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
# imports.py: filter data, folder bernama dari server, dan batas ukuran
import tarfile
import uuid
from pathlib import Path
UPLOAD_ROOT = Path("/srv/app/imports")
MAX_MEMBERS = 10_000
MAX_TOTAL_BYTES = 500 * 1024 * 1024
# tolak arsip dengan terlalu banyak anggota atau byte
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' memblokir .., nama absolut, tautan keluar, dan file perangkat
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
Daftar Periksa Penguatan Sistem Rekayasa
- Berikan
filter="data"pada setiap panggilanextractall()danextract(), dan gagalkan CI ketika Bandit B202 atau aturan Semgrep menemukan panggilan tanpafilter=. - Ekstrak ke folder baru yang dinamai server, misalnya UUID, jangan pernah ke path yang disusun dari data request.
- Periksa jumlah anggota dan total ukuran tak terkompresi sebelum mengekstrak, dan tolak arsip yang melebihi batas.
- Jalankan worker ekstraksi sebagai pengguna tanpa akses tulis ke kode atau konfigurasi aplikasi.
- Tambahkan tes CI dengan arsip berisi
../outside.txtdan anggota ber-path absolut, dan harapkantarfile.FilterError.