● CWE-22 · CVE-2007-4559

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 tarfile verhinderte nicht, dass Mitgliedsnamen das Extraktionsverzeichnis verlassen. Das Problem war seit 2007 bekannt, und 2022 schätzte Trellix, dass rund 350.000 Open-Source-Projekte extractall() auf nicht vertrauenswürdige Archive anwendeten.
Mitgliedsnamen mit .. oder absoluten Pfaden
Ein tar-Eintrag namens ../outside.txt oder /etc/cron.d/job zeigt nach dem Zusammenfügen mit dem Ziel nach außerhalb. Ohne Filter schreibt extractall() 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

Schritt 1

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.

Schritt 2

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.

Schritt 3

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.

Schritt 4

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

VERWUNDBARE IMPLEMENTIERUNG
# 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
GEHÄRTETER SICHERHEITS-PATCH
# 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

Quellen

← Zum vollständigen Sicherheitsverzeichnis Alle Anleitungen zu Schwachstellen →