In this article
Sichere Pfadverarbeitung: Warum sichere Dateisystemoperationen schwieriger sind als gedacht
Path-Traversal-Schwachstellen treten immer wieder auf. Die Datei-E/A-Funktionen der Standardbibliotheken, die die meisten Programmiersprachen bereitstellen, wurden auf Komfort ausgelegt, nicht auf den Einsatz in Umgebungen mit Angreifern. Wenn Ihr Code in einer Sandbox, auf einem gemeinsam genutzten System oder zusammen mit nicht vertrauenswürdigen Eingaben ausgeführt wird, wird diese Lücke zur Verletzung einer Sicherheitsgrenze.
Das Sicherheitsteam von Snyk ist diesem Muster wiederholt in verschiedenen Ökosystemen begegnet:
Schwerwiegende Schwachstellen in Incus, bei denen Newline-Injection mit Symlink-Angriffen kombiniert wurde, um beliebige Dateien zu schreiben und Root-Rechte auf Host-Ebene zu erlangen
Eine Privilegieneskalationskette in NixOS, bei der Race-Conditions im Nix-Store ausgenutzt wurden, um Schreibzugriff auf Pfade zu behalten, die unveränderlich sein sollten
Privilegieneskalation in Ubuntu 24.04 vom Standardbenutzer zu Root durch die Kombination von Dateisystemmanipulation mit Fehlern in privilegierten Komponenten
Dabei handelt es sich nicht um exotische Angriffstechniken, sondern um die Folge von Code, der einen Pfad prüft und ihn anschließend verwendet. Dazwischen liegt ein Zeitfenster, in dem ein Angreifer ein sicheres Ziel durch ein schädliches ersetzen kann.
Die drei Schwachstellenklassen
Path Traversal
Der Klassiker. Vom Benutzer bereitgestellte Eingaben wie ../../etc/passwd verlassen das vorgesehene Verzeichnis. Die meisten Entwickler wissen, dass sie darauf achten müssen. Trotzdem tritt das Problem regelmäßig auf, weil die Pfadvalidierung schwieriger ist, als es scheint. Einfache Zeichenfolgenprüfungen berücksichtigen weder URL-Kodierung und Nullbytes noch Unicode-Normalisierung oder plattformspezifische Pfadtrennzeichen.
# Looks safe, isn't safe
def read_user_file(filename):
path = os.path.join("/app/uploads", filename)
if not path.startswith("/app/uploads"):
raise ValueError("Access denied")
return open(path).read()
# filename = "../../etc/passwd"
# path = "/app/uploads/../../etc/passwd"
# os.path.join resolves this, but startswith still passes on some inputsSymlink-Angriffe
Ein Angreifer erstellt einen symbolischen Link, der von einem Ort, auf den Ihr Code zugreifen soll, zu einem Ort verweist, auf den er nicht zugreifen sollte. Folgt Ihr Code Symlinks – was auf den meisten Systemen standardmäßig der Fall ist –, liest er vom vom Angreifer gewählten Ziel oder schreibt dorthin.
Besonders gefährlich ist das in gemeinsam genutzten Umgebungen, temporären Verzeichnissen und Container-/Sandbox-Grenzen, wo ein Angreifer möglicherweise eingeschränkten Schreibzugriff auf das Dateisystem hat.
TOCTOU-Race-Conditions
Time-of-Check to Time-of-Use. Ihr Code validiert einen Pfad und führt anschließend eine Operation damit aus. Zwischen diesen beiden Vorgängen tauscht ein Angreifer das Ziel aus. Die Prüfung ist erfolgreich, aber die Operation betrifft eine andere Datei.
# Classic TOCTOU vulnerability
if os.path.isfile(path) and not os.path.islink(path):
# Attacker swaps path for a symlink right here
with open(path, 'w') as f:
f.write(data) # Now writing to attacker's targetDas grundlegende Problem: Pfadbasierte Operationen unterliegen auf Mehrbenutzer- oder Mehrprozesssystemen grundsätzlich Race-Conditions. Zwischen zwei Systemaufrufen, die sich auf denselben Pfad beziehen, kann sich der Zustand des Dateisystems ändern.
Die Lösung: dateideskriptorbasierte Operationen
Der richtige Ansatz kombiniert openat(), O_NOFOLLOW und die f*-Familie von Systemaufrufen (fstat, fchmod, fchown). Entscheidend ist: Sobald Sie einen geöffneten Datei-Deskriptor haben, verweist dieser auf ein bestimmtes Dateisystemobjekt – unabhängig davon, was mit dem Pfad geschieht.
Das Vorgehen sieht folgendermaßen aus:
Beginnen Sie mit einem bekannten, sicheren Verzeichnis (etwa
/oder einer Sandbox-Wurzel)Öffnen Sie jede Pfadkomponente einzeln mit
openat()und dem Datei-Deskriptor des vorherigen VerzeichnissesVerwenden Sie
O_NOFOLLOW, um Symlinks bei jedem Schritt zurückzuweisenValidieren Sie jede Komponente (kein
.., keine Symlinks, korrekte Berechtigungen)Verwenden Sie den finalen Datei-Deskriptor für alle nachfolgenden Operationen
So werden TOCTOU-Race-Conditions verhindert, denn nach dem Öffnen verwenden Sie den Pfad nicht mehr. Alle Operationen erfolgen über den Datei-Deskriptor, der laut Kernel unabhängig von Pfadänderungen auf denselben Inode verweist.
Wie Programmiersprachen damit umgehen
C/C++: volle Kontrolle
C und C++ bieten direkten Zugriff auf openat() und O_NOFOLLOW, sodass sich eine korrekte Implementierung unkompliziert umsetzen lässt (wenn auch mit viel Code). Googles Lösung für ChromeOS (libbrillo/brillo/files/safe_fd.cc) ist eine gute Referenzimplementierung.
int safe_open(int dirfd, const char *component) {
return openat(dirfd, component,
O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
}Go: Unterstützung in der Standardbibliothek (seit Go 1.24)
Go hat os.Root in die Standardbibliothek aufgenommen. Damit lassen sich Dateien innerhalb einer Verzeichnisstruktur sicher und ohne Race-Conditions aufrufen. Das ist der Goldstandard für die Unterstützung auf Sprachebene.
// Go 1.24+ — safe by default
root, err := os.OpenRoot("/sandbox")
if err != nil {
log.Fatal(err)
}
defer root.Close()
f, err := root.Open("user/data.txt")
// Guaranteed to stay within /sandbox, TOCTOU-proofGos Blogbeitrag zum Design von os.Root lohnt sich, um die Gründe und die berücksichtigten Sonderfälle nachzuvollziehen.
Rust: gute Primitive, manuelle Zusammensetzung
Rust stellt openat über den nix-Crate bereit und unterstützt O_NOFOLLOW. Der cap-std-Crate ermöglicht dateisystemzugriffe auf Basis von Capabilities und verhindert Path Traversal bereits durch sein Design.
use cap_std::fs::Dir;
let root = Dir::open_ambient_dir("/sandbox", cap_std::ambient_authority())?;
let file = root.open("user/data.txt")?;
// Constrained to /sandboxPython: teilweise Unterstützung
Python 3.11+ bietet os.open() mit den Parametern O_NOFOLLOW und dir_fd. Das vollständige sichere Traversal-Muster erfordert jedoch manuelle Arbeit. Ein Pendant zu Gos os.Root gibt es in der Standardbibliothek nicht.
import os
def safe_open_under(root_path, relative_path):
root_fd = os.open(root_path, os.O_RDONLY | os.O_DIRECTORY)
try:
components = relative_path.split('/')
current_fd = root_fd
for component in components[:-1]:
if component in ('', '.', '..'):
raise ValueError(f"Invalid path component: {component}")
next_fd = os.open(
component,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=current_fd
)
if current_fd != root_fd:
os.close(current_fd)
current_fd = next_fd
return os.open(
components[-1],
os.O_RDONLY | os.O_NOFOLLOW,
dir_fd=current_fd
)
finally:
os.close(root_fd)Node.js: praktisch unmöglich?
Hier wird es schwierig. Node.js stellt weder openat() noch O_NOFOLLOW in der Standardbibliothek bereit. Das Modul fs arbeitet ausschließlich mit Pfaden, nicht mit Datei-Deskriptoren. Reines Node.js ermöglicht kein TOCTOU-sicheres Durchlaufen von Verzeichnissen ohne native Add-ons.
Das Beste, was Sie tun können, ist fs.realpath() gefolgt von einer Präfixprüfung. Das ist jedoch grundsätzlich anfällig für Race-Conditions. Ein natives Add-on mit N-API könnte openat() verfügbar machen, bringt aber Anforderungen an die Kompilierung und plattformspezifische Komplexität mit sich.
Für eine Laufzeitumgebung, die häufig in CLI-Tools, Build-Systemen und containerisierten Umgebungen zum Einsatz kommt – in denen Pfadsicherheit wichtig ist –, stellt das eine erhebliche Lücke dar.
Wann ist das relevant?
Nicht jede Anwendung benötigt TOCTOU-sichere Dateioperationen. Wenn Ihr Code nur Dateien über fest codierte Pfade öffnet oder ausschließlich in einer Einzelbenutzerumgebung ohne nicht vertrauenswürdige Eingaben läuft, sind die Funktionen der Standardbibliothek ausreichend.
Auf eine sichere Pfadverarbeitung sollten Sie achten, wenn:
Ihr Code in einer Sandbox oder einem Container ausgeführt wird und Dateisystemoperationen Vertrauensgrenzen überschreiten
Ihr Code vom Benutzer bereitgestellte Dateipfade verarbeitet (Uploads, Konfigurationsdateien, Plugin-Verzeichnisse)
Ihr Code mit erhöhten Berechtigungen ausgeführt wird und auf Pfade zugreift, die von nicht privilegierten Benutzern beeinflusst werden
Ihr Code in gemeinsam genutzten Umgebungen ausgeführt wird (Mandantensysteme, CI/CD-Pipelines, Build-Systeme)
Ihr Code verarbeitet temporäre Dateien in allgemein beschreibbaren Verzeichnissen wie
/tmp
Empfohlene Maßnahmen
Prüfen Sie Ihre Codebasis auf pfadbasierte Dateioperationen, die Benutzereingaben verarbeiten
Ersetzen Sie pfadbasierte Prüfungen (etwa
startswith()nachrealpath()) nach Möglichkeit durch dateideskriptorbasiertes TraversalVerwenden Sie in Go 1.24+
os.Rootfür alle Dateizugriffe in einer SandboxZiehen Sie in Rust den
cap-std-Crate für dateisystemzugriffe auf Basis von Capabilities in BetrachtBerücksichtigen Sie in Node.js die Einschränkung und setzen Sie mehrschichtige Schutzmaßnahmen um (Eingabevalidierung, chroot, AppArmor-/SELinux-Profile)
Verwenden Sie Snyk Code, um Path-Traversal-Muster während der Entwicklung in Ihrer Codebasis zu erkennen
Legen versteckte Schwachstellen bei der Dateiverarbeitung wie Path Traversal Ihre Anwendungen unbemerkt einem größeren Risiko aus? Laden Sie das Whitepaper The AI Security Crisis in Your Python Environment herunter und erfahren Sie, wie moderne Teams diese Risiken sowohl in KI-generiertem als auch von Entwicklern geschriebenem Code verhindern.
WHITEPAPER
Die KI-Sicherheitskrise in Ihrer Python-Umgebung
Angesichts des rasanten Entwicklungstempos: Wissen Sie wirklich, worauf Ihre KI-Umgebung zugreifen kann?