Jede Crate in deinem Dependency-Tree kann /etc/passwd öffnen, in dein ~/.ssh-Verzeichnis schreiben oder jede Datei in deinem Projekt auflisten. Die Standardbibliothek von Rust fragt nicht um Erlaubnis. Sie geht davon aus, dass jeder Code, der std::fs::File::open aufrufen kann, berechtigt ist, jeden Pfad zu berühren, den das Betriebssystem erlaubt.
cap-std ändert diese Annahme. Es ist ein Drop-in-Ersatz für die std-I/O-Module von Rust, der umgebungsbasierte Berechtigungen durch explizite Capabilities ersetzt. Wenn du eine Datei öffnen willst, brauchst du zuerst eine Capability, die beweist, dass du Zugriff auf das Verzeichnis hast, in dem sie liegt.
Das ist wichtiger, als du vielleicht denkst. Supply-Chain-Angriffe brauchen nicht immer ausgeklügelte Exploits. Manchmal reicht ein Build-Skript oder eine transitive Dependency, die heimlich Dateien exfiltriert, während deine Tests laufen. Dieses Risiko auf Bibliotheksebene zu sandboxen, ohne Container oder VMs, ermöglicht cap-std.
Wie Capability-basierter Dateisystemzugriff tatsächlich aussieht
In Standard-Rust ist das Öffnen einer Datei ein One-Liner ohne Voraussetzungen:
use std::fs::File;
// Any code, anywhere, can do this
let file = File::open("/etc/passwd")?;
Der Pfad ist absolut und die Berechtigung ist umgebungsbasiert. Das Betriebssystem prüft die Berechtigungen, aber das Programm selbst muss nie nachweisen, dass es diesen Pfad überhaupt hätte konstruieren dürfen.
cap-std ersetzt dies durch ein Zweischritt-Modell. Du beginnst mit einer Capability, typischerweise einem Directory-Handle, und kannst nur Pfade öffnen, die relativ zu diesem Verzeichnis sind:
use cap_std::fs::Dir;
use std::path::Path;
// Open the current working directory as a capability
let cwd = Dir::open_ambient_dir(".", cap_std::ambient_authority())?;
// Now we can only open files inside this directory tree
let file = cwd.open("config/app.toml")?;
Beachte den Aufruf von open_ambient_dir. Das ist die Fluchttür. Sie wandelt einen umgebungsbasierten Pfad in eine Capability um und ist absichtlich ausführlich und leicht zu auditieren. Sobald du ein Dir hast, gibt es keine open-Methode, die einen absoluten Pfad akzeptiert. Die API erlaubt das schlicht nicht.
Wie cap-std std spiegelt, ohne dessen Sicherheitsmodell zu kopieren
cap-std ist als direkter Ersatz für std::fs, std::net und std::os::unix::net strukturiert. Die Typen sind absichtlich vertraut. cap_std::fs::File umschließt std::fs::File. cap_std::fs::Dir ist der neue Einstiegspunkt, in etwa vergleichbar mit der Arbeit in einem chroot, nur dass es vom Type System erzwungen wird statt durch einen privilegierten OS-Aufruf.
Die Crate erreicht dies durch eine Abstraktion auf niedrigerer Ebene namens cap-primitives. Unter der Haube nutzt cap-std openat-artige Systemaufrufe auf Unix und ähnliche restricted-relative Operationen auf Windows. Sie ruft nie die uneingeschränkte File::open der Standardbibliothek auf. Auf Linux verwendet sie openat2 mit RESOLVE_BENEATH, wenn verfügbar, um Symlink-Escapes zu verhindern. Auf Windows nutzt sie NtCreateFile mit eingeschränkten Flags.
Das ist kein Wrapper um Container oder seccomp. Es ist eine Reimplementierung der Standard-Dateisystem-APIs, die gefährliche Operationen schlicht weglässt.
Der Typ Dir unterstützt das meiste, was du erwarten würdest: open, create_dir, rename, remove_file, read_dir und so weiter. Der entscheidende Unterschied ist, dass jede Operation relativ zu diesem Directory-Handle ist. Wenn du eine Datei außerhalb des Baums verschieben willst, brauchst du zwei Dir-Capabilities, eine für die Quelle und eine für das Ziel. Die API zwingt dich, den Zugriffsnachweis mit dir zu tragen.
Die Portabilitätstricks, die cap-std verwendet
Dateisystem-Capability-Modelle unterscheiden sich je nach Betriebssystem. Linux hat openat2. FreeBSD hat cap_rights_limit und O_RESOLVE_BENEATH. Windows hat handle-basierte Zugriffskontrolle, aber kein direktes Äquivalent zu RESOLVE_BENEATH. macOS bietet die eingeschränkteste Unterstützung der großen Plattformen.
cap-std geht das durch eine Portabilitätsschicht an. Auf Systemen mit starker Kernel-Unterstützung nutzt es die nativen Restriktionsmechanismen. Auf Systemen ohne diese fällt es auf eine Userspace-Implementierung zurück, die Symlinks manuell auflöst und validiert, dass jede Komponente eines relativen Pfads innerhalb der Capability-Boundary bleibt.
Dieser Fallback ist teurer, aber er bedeutet, dass dein Sicherheitsmodell portabel ist. Eine WASI-Sandbox, ein Linux-Server und das MacBook eines Entwicklers können alle dieselben Capability-Boundaries erzwingen, ohne plattformspezifischen Code in deiner Anwendung.
Wie die Verwendung von cap-std in einem echten Projekt aussieht
Die Konvertierung eines bestehenden Projekts ist weniger schmerzhaft, als du vielleicht erwartest, weil die APIs absichtlich nah an std sind. Die Hauptänderung ist, dass du aufhörst, &Path oder &str als Einheit der Dateisystemadressierung herumzureichen. Stattdessen übergibst du &Dir.
Hier ist ein vereinfachtes Beispiel für das Lesen einer Konfiguration aus einem Verzeichnis, das der Aufrufer kontrolliert:
use cap_std::fs::Dir;
use std::io::{self, Read};
pub fn load_config(dir: &Dir, name: &str) -> io::Result<String> {
let mut file = dir.open(name)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
Die Funktion load_config kann nicht auf Dateien außerhalb des übergebenen Verzeichnisses zugreifen. Sie muss nicht wissen, wo dieses Verzeichnis auf dem Datenträger liegt. Das macht es trivial, sie isoliert zu testen:
use cap_std::fs::Dir;
use cap_tempfile::TempDir;
#[test]
fn test_load_config() -> std::io::Result<()> {
let tmp = TempDir::new(Default::default())?;
let dir = tmp.dir();
{
let mut f = dir.create("app.toml")?;
std::io::Write::write_all(&mut f, b"key = \"value\"")?;
}
let config = load_config(dir, "app.toml")?;
assert!(config.contains("value"));
Ok(())
}
cap-tempfile stellt ein temporäres Verzeichnis als Capability bereit, sodass selbst deine Tests nie umgebungsbasierte Berechtigungen gewähren. Die Funktion load_config würde fehlschlagen, wenn sie versuchte, ../etc/passwd oder einen absoluten Pfad zu öffnen, unabhängig davon, was der Test oder Aufrufer bereitstellt.
Was kaputtgeht, wenn du wechselst
Die größte Einschränkung ist die Ökosystem-Kompatibilität. Die meisten Rust-Crates, die das Dateisystem berühren, akzeptieren &Path oder PathBuf. Sie erwarten umgebungsbasierte Berechtigungen. Wenn du cap-std beispielsweise mit einer Konfigurationsparser-Crate verwenden willst, die Include-Dateien vom Datenträger liest, muss diese Crate cap-std-aware sein, oder du musst die Dateien selbst lesen und String-Inhalte an den Parser übergeben.
Das ist dieselbe Migrationsgeschichte wie bei async Rust. Genau wie eine Funktion, die std::fs::File annimmt, nicht aus async-Code aufgerufen werden kann, der einen async File-Handle erwartet, kann eine Funktion, die &Path annimmt, nicht mit &Dir aufgerufen werden. Die Grenze ist klar, aber real.
Es gibt auch Operationen, die cap-std absichtlich nicht unterstützt. Du kannst keine Hard Links erstellen, die einen Verzeichnisbaum verlassen. Du kannst nicht beliebigen Symlinks folgen, die außerhalb der Capability-Boundary zeigen. Du kannst std::env::current_dir nicht aufrufen und annehmen, dass es in einem Capability-basierten Programm irgendetwas bedeutet. Das sind keine Bugs. Das ist das Sicherheitsmodell.
Die Performance ist normalerweise kein Problem. Auf Linux mit openat2 ist der Overhead ein paar zusätzliche Flags für einen bestehenden Syscall. Auf Plattformen, die den Userspace-Fallback nutzen, ist die Pfadauflösung langsamer, aber im Vergleich zum tatsächlichen I/O in der Regel immer noch vernachlässigbar.
Wann sich cap-std den Aufwand lohnt
Du brauchst cap-std wahrscheinlich nicht für jedes CLI-Tool oder jeden Webserver. Wenn du deinen Dependencies vertraust und dein Threat-Model auf externe Angreifer statt auf bösartige Crates ausgerichtet ist, sind Container und normale OS-Berechtigungen ausreichend.
cap-std glänzt in einigen spezifischen Szenarien:
Plugin-Systeme. Wenn deine Anwendung nicht vertrauenswürdige WASM-Module oder native Plugins lädt, erlaubt dir cap-std, jedem Plugin eine Dir-Capability zu gewähren, die seine Sandbox repräsentiert. Das Plugin kann nicht ausbrechen, ohne einen Kernel-Exploit zu nutzen, weil die API schlicht nicht das Konzept „irgendeine Datei öffnen” ausdrückt.
Build-Tools und Package-Manager. Diese führen beliebigen Code aus dem Internet aus. Ein build.rs-Skript, das cap-std verwendet, kann deine SSH-Schlüssel nicht lesen, es sei denn, du übergibst ihm explizit eine Capability für dein Home-Verzeichnis.
WASI-Targets. Die WebAssembly System Interface basiert auf Capabilities. Das API-Design von cap-std hat WASI direkt beeinflusst, und die Verwendung von cap-std macht die Portierung auf WASI unkompliziert, weil das Sicherheitsmodell bereits übereinstimmt.
Testen und Reproduzierbarkeit. Tests, die &Dir anstelle des aktuellen Arbeitsverzeichnisses verwenden, sind standardmäßig hermetisch. Du musst nicht chdir aufrufen und den Zustand wiederherstellen. Du übergibst dem Test einfach eine temporäre Directory-Capability.
Erste Schritte
Füge cap-std zu deiner Cargo.toml hinzu:
[dependencies]
cap-std = "3.0"
cap-tempfile = "3.0"
Wähle ein Modul, das Dateien liest, ändere seine öffentliche API so, dass sie &Dir statt &Path akzeptiert, und aktualisiere die Aufrufer. Du musst nicht alles auf einmal migrieren. cap-std und std::fs können im selben Binary koexistieren. Die Migration ist pro Modul optional.
Wenn du eine Bibliothek pflegst, die Dateien liest, erwäge, eine auf cap_std::fs::Dir basierende API neben deiner bestehenden pfadbasierten API hinzuzufügen. Deine WASI-Nutzer werden es dir danken, und deine sicherheitsbewussten Nutzer werden ihre Anwendungen einfacher sandboxen können.
Das cap-std-Repository befindet sich unter github.com/bytecodealliance/cap-std. Die Crate-Dokumentation enthält Hinweise zur Plattformunterstützung und die vollständige API-Oberfläche. Beginne mit Dir::open_ambient_dir als Einstiegspunkt und versuche dann, jeden anderen Aufruf von std::fs, der einen absoluten Pfad akzeptiert, zu entfernen. Du wirst überrascht sein, wie viele umgebungsbasierte Berechtigungen dein Code mit sich herumgetragen hat.