Path Traversal in Pythons tarfile.extractall (CVE-2007-4559): Wie es passiert und wie man es behebt
Vor den Extraktionsfiltern schrieb tarfile.extractall() jedes Mitglied an den im Archiv gespeicherten Pfad, sodass Namen mit .., absolute Pfade oder Links Dateien außerhalb des Zielordners ablegen konnten. Wie das Argument filter="data" aus PEP 706 das verhindert, warum das Ziel nicht aus Nutzereingaben stammen darf und welche Größengrenzen dazugehören, mit einem Python-Beispiel.
Einfache Erklärung (ELI5)
Ein Umzugsunternehmen packt Kartons in Ihrer neuen Wohnung aus und stellt jeden Gegenstand dorthin, wo sein Etikett es sagt. Ein Etikett mit „Küche, dann durch die Wohnungstür, dann ins Schlafzimmer des Nachbarn“ wird wörtlich befolgt, weil die Packer nur Etiketten lesen und nie prüfen, ob die Adresse noch in Ihrer Wohnung liegt. Ein Archiv funktioniert genauso: Jede Datei trägt ihren eigenen Zielpfad. Die Lösung ist ein Vorarbeiter, der jedes Etikett vorher liest und alles ablehnt, was die Wohnung verlassen würde, ebenso alles, was kein gewöhnlicher Karton ist, etwa ein Schild, das woandershin zeigt.
Kernkonzepte & Begriffe
- CVE-2007-4559
- Pythons Modul
tarfileverhinderte nicht, dass Mitgliedsnamen das Extraktionsverzeichnis verlassen. Das Problem war seit 2007 bekannt, und 2022 schätzte Trellix, dass rund 350.000 Open-Source-Projekteextractall()auf nicht vertrauenswürdige Archive anwendeten. - Mitgliedsnamen mit .. oder absoluten Pfaden
- Ein tar-Eintrag namens
../outside.txtoder/etc/cron.d/jobzeigt nach dem Zusammenfügen mit dem Ziel nach außerhalb. Ohne Filter schreibtextractall()mit den Rechten des Prozesses an diesen Pfad. - Symbolische und harte Links als Mitglieder
- Ein Archiv kann zuerst im Ziel einen Link anlegen, der woandershin zeigt, und dann eine normale Datei durch ihn schreiben. Die Prüfung der Namen allein übersieht das, deshalb prüfen Filter auch Linkziele.
- Extraktionsfilter (PEP 706)
filter="data"lehnt absolute Namen,..-Ausbrüche, Links aus dem Ziel heraus und Gerätedateien ab und entfernt Set-User-ID- sowie Ausführungsrechte für andere. Er ist ab Python 3.12 und in den Sicherheitsversionen 3.8 bis 3.11 verfügbar und wird in Python 3.14 zum Standard.- Ressourcengrenzen
- Filter begrenzen keine Größe. Ein kleines komprimiertes Archiv kann sich auf Millionen Mitglieder oder viele Gigabyte ausdehnen, daher Anzahl und Gesamtgröße vor dem Entpacken zählen.
Schritt-für-Schritt Angriffsablauf
Die App nimmt ein hochgeladenes Archiv an
Ein Datenimport, ein Plugin-Installer oder ein Handler für CI-Artefakte erhält ein .tar.gz von einem Nutzer.
Ein Mitgliedsname zeigt aus dem Ordner heraus
Der gespeicherte Pfad eines Eintrags steigt aus dem Ziel heraus oder ist absolut. Ein Testarchiv mit einem Mitglied namens ../outside.txt zeigt den Fall.
extractall schreibt an den aufgelösten Pfad
Die Datei landet dort, wohin der Name führt, und das Ziel selbst kann aus einer vom Nutzer gelieferten job_id stammen.
Anwendungsdateien werden ersetzt
Überschriebene Konfiguration, Templates oder Skripte laufen beim nächsten Laden durch die Anwendung, so wird aus diesem Path Traversal Codeausführung.
Quellcode: Verwundbar vs. Sicher
# imports.py: jedes Mitglied landet dort, wohin sein gespeicherter Name zeigt
import tarfile
from pathlib import Path
UPLOAD_ROOT = Path("/srv/app/imports")
# vom Aufrufer gewählter Ordner, kein Filter, keine Größengrenzen
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: data-Filter, vom Server benannter Ordner und Größengrenzen
import tarfile
import uuid
from pathlib import Path
UPLOAD_ROOT = Path("/srv/app/imports")
MAX_MEMBERS = 10_000
MAX_TOTAL_BYTES = 500 * 1024 * 1024
# Archive mit zu vielen Mitgliedern oder Bytes ablehnen
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' blockiert .., absolute Namen, Links nach außen und Gerätedateien
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
Checkliste für Engineering & Systemsicherheit
- Jedem Aufruf von
extractall()undextract()filter="data"übergeben und die CI scheitern lassen, wenn Bandit B202 oder eine Semgrep-Regel einen Aufruf ohnefilter=findet. - In einen neuen, vom Server benannten Ordner entpacken, etwa eine UUID, nie in einen aus Request-Daten gebauten Pfad.
- Vor dem Entpacken Anzahl der Mitglieder und unkomprimierte Gesamtgröße prüfen und Archive über den Grenzen ablehnen.
- Den Entpack-Worker unter einem Nutzer ohne Schreibrechte auf Anwendungscode oder Konfiguration ausführen.
- Einen CI-Test mit einem Archiv ergänzen, das
../outside.txtund ein Mitglied mit absolutem Pfad enthält, undtarfile.FilterErrorerwarten.