Crate apa pun di pohon dependency Anda bisa membuka /etc/passwd, menulis ke direktori ~/.ssh Anda, atau menghitung setiap berkas di proyek Anda. Standard library Rust tidak meminta izin. Ia mengasumsikan bahwa kode apa pun yang memiliki kemampuan untuk memanggil std::fs::File::open berwenang untuk menyentuh path apa pun yang diizinkan OS.

cap-std mengubah asumsi tersebut. Ia adalah pengganti langsung untuk module I/O std Rust yang menggantikan otoritas ambient dengan capability eksplisit. Jika Anda ingin membuka suatu berkas, Anda terlebih dahulu memerlukan capability yang membuktikan bahwa Anda memiliki akses ke direktori yang menampungnya.

Ini lebih penting daripada yang Anda kira. Serangan supply chain tidak selalu memerlukan eksploitasi canggih. Terkadang mereka hanya memerlukan build script atau dependency transitif yang diam-diam mengeksfiltrasi berkas saat tes Anda berjalan. Melakukan sandbox terhadap risiko tersebut di tingkat library, tanpa container atau VM, itulah yang dimungkinkan oleh cap-std.

Seperti apa akses filesystem berbasis capability

Di Rust standar, membuka berkas adalah satu baris tanpa prasyarat apa pun:

use std::fs::File;

// Any code, anywhere, can do this
let file = File::open("/etc/passwd")?;

Path-nya absolut, dan otoritasnya ambient. Sistem operasi memeriksa izin, tetapi program itu sendiri tidak pernah harus membuktikan bahwa seharusnya ia memang bisa membangun path tersebut sejak awal.

cap-std menggantinya dengan model dua langkah. Anda memulai dengan capability, biasanya sebuah directory handle, dan Anda hanya bisa membuka path yang relatif terhadap direktori tersebut:

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")?;

Perhatikan pemanggilan open_ambient_dir. Itu adalah jalan keluar. Ia mengubah path ambient menjadi capability, dan sengaja dibuat bertele-tele serta mudah diaudit. Setelah Anda memiliki Dir, tidak ada metode open yang menerima path absolut. API-nya sama sekali tidak mengizinkannya.

Bagaimana cap-std mencerminkan std tanpa menyalin model keamanannya

cap-std disusun sebagai pengganti langsung untuk std::fs, std::net, dan std::os::unix::net. Tipenya sengaja dibuat familiar. cap_std::fs::File membungkus std::fs::File. cap_std::fs::Dir adalah titik masuk baru, kurang lebih mirip dengan bekerja di dalam chroot kecuali diberlakukan oleh type system dan bukan oleh pemanggilan OS yang memiliki hak istimewa.

Crate ini mencapainya melalui abstraksi tingkat bawah yang disebut cap-primitives. Di balik layar, cap-std menggunakan system call bergaya openat di Unix dan operasi restricted-relative serupa di Windows. Ia tidak pernah memanggil File::open yang tidak dibatasi dari standard library. Di Linux, ia menggunakan openat2 dengan RESOLVE_BENEATH jika tersedia untuk mencegah pelarian symlink. Di Windows, ia menggunakan NtCreateFile dengan flag terbatas.

Ini bukan wrapper di sekitar container atau seccomp. Ini adalah reimplementasi API filesystem standar yang secara sederhana menghilangkan operasi berbahaya.

Tipe Dir mendukung sebagian besar hal yang Anda harapkan: open, create_dir, rename, remove_file, read_dir, dan sebagainya. Perbedaan kuncinya adalah setiap operasi bersifat relatif terhadap directory handle tersebut. Jika Anda ingin memindahkan berkas ke luar pohon direktori, Anda memerlukan dua capability Dir, satu untuk sumber dan satu untuk tujuan. API ini memaksa Anda untuk membawa bukti akses bersama Anda.

Trik portabilitas yang digunakan cap-std

Model capability filesystem bervariasi antar sistem operasi. Linux memiliki openat2. FreeBSD memiliki cap_rights_limit dan O_RESOLVE_BENEATH. Windows memiliki access control berbasis handle tetapi tidak memiliki padanan langsung dari RESOLVE_BENEATH. macOS memiliki dukungan paling terbatas di antara platform utama.

cap-std menangani ini melalui lapisan portabilitas. Di sistem dengan dukungan kernel yang kuat, ia menggunakan mekanisme pembatasan native. Di sistem tanpanya, ia fallback ke implementasi userspace yang menyelesaikan symlink secara manual dan memvalidasi bahwa setiap komponen path relatif tetap berada di dalam capability boundary.

Fallback ini lebih mahal, tetapi berarti model keamanan Anda portabel. Sandbox WASI, server Linux, dan MacBook pengembang semuanya bisa menegakkan capability boundary yang sama tanpa kode spesifik platform di aplikasi Anda.

Seperti apa penggunaan cap-std di proyek nyata

Mengonversi proyek yang ada tidak sesakit yang Anda kira, karena API-nya sengaja dibuat mendekati std. Perubahan utamanya adalah Anda berhenti meneruskan &Path atau &str sebagai unit alamat filesystem. Sebagai gantinya, Anda meneruskan &Dir.

Berikut adalah contoh sederhana membaca konfigurasi dari direktori yang dikendalikan oleh pemanggil:

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)
}

Fungsi load_config tidak bisa mengakses berkas di luar direktori yang diberikan kepadanya. Ia tidak perlu tahu di mana direktori tersebut berada di disk. Ini membuat pengujian secara terisolasi menjadi sangat mudah:

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 menyediakan direktori sementara sebagai capability, sehingga bahkan tes Anda tidak pernah memberikan otoritas ambient. Fungsi load_config akan gagal jika mencoba membuka ../etc/passwd atau path absolut apa pun, terlepas dari apa yang diberikan oleh tes atau pemanggil.

Apa yang rusak saat Anda beralih

Keterbatasan terbesarnya adalah kompatibilitas ekosistem. Sebagian besar crate Rust yang menyentuh filesystem menerima &Path atau PathBuf. Mereka mengharapkan otoritas ambient. Jika Anda ingin menggunakan cap-std dengan, misalnya, crate parsing konfigurasi yang membaca file include dari disk, crate tersebut perlu menyadari cap-std atau Anda perlu membaca berkasnya sendiri dan meneruskan konten string ke parser.

Ini adalah cerita migration yang sama seperti async Rust. Sama seperti fungsi yang menerima std::fs::File tidak bisa dipanggil dari kode async yang mengharapkan async file handle, fungsi yang menerima &Path tidak bisa dipanggil dengan &Dir. Batasannya jelas tetapi nyata.

Ada juga operasi yang sengaja tidak didukung oleh cap-std. Anda tidak bisa membuat hard link yang keluar dari pohon direktori. Anda tidak bisa mengikuti symlink sembarangan yang menunjuk ke luar capability boundary. Anda tidak bisa memanggil std::env::current_dir dan mengasumsikan bahwa itu berarti apa pun di program berbasis capability. Ini bukan bug. Itulah model keamanannya.

Performa biasanya bukan masalah. Di Linux dengan openat2, overhead-nya adalah beberapa flag tambahan pada syscall yang sudah ada. Di platform yang menggunakan fallback userspace, resolusi path lebih lambat, tetapi biasanya masih bisa diabaikan dibandingkan dengan I/O yang sebenarnya.

Kapan cap-std sepadan dengan friksinya

Anda mungkin tidak memerlukan cap-std untuk setiap CLI tool atau web server. Jika Anda mempercayai dependency Anda dan threat model Anda adalah penyerang eksternal dan bukan crate berbahaya, container dan izin OS normal sudah cukup baik.

cap-std bersinar di beberapa skenario spesifik:

Sistem plugin. Jika aplikasi Anda memuat module WASM yang tidak tepercaya atau plugin native, cap-std memungkinkan Anda memberikan setiap plugin capability Dir yang merepresentasikan sandbox-nya. Plugin tidak bisa keluar tanpa eksploitasi kernel, karena API-nya sama sekali tidak mengekspresikan konsep “membuka berkas apa pun.”

Build tool dan package manager. Ini menjalankan kode sembarangan dari internet. Sebuah skrip build.rs yang menggunakan cap-std tidak bisa membaca kunci SSH Anda kecuali Anda secara eksplisit memberikannya capability ke direktori home Anda.

Target WASI. WebAssembly System Interface dibangun di atas capability. Desain API cap-std secara langsung memengaruhi WASI, dan menggunakan cap-std membuat porting ke WASI menjadi mudah karena model keamanannya sudah selaras.

Pengujian dan reprodusibilitas. Tes yang menerima &Dir alih-alih mengandalkan direktori kerja saat ini bersifat hermetic secara default. Anda tidak perlu chdir dan memulihkan state. Anda hanya memberikan tes sebuah capability direktori sementara.

Memulai

Tambahkan cap-std ke Cargo.toml Anda:

[dependencies]
cap-std = "3.0"
cap-tempfile = "3.0"

Pilih satu module yang membaca berkas, ubah API publiknya untuk menerima &Dir alih-alih &Path, dan perbarui pemanggilnya. Anda tidak harus memigrasikan semuanya sekaligus. cap-std dan std::fs bisa berdampingan dalam biner yang sama. Migrasinya bersifat opt-in per module.

Jika Anda memelihara library yang membaca berkas, pertimbangkan untuk menambahkan API berbasis cap_std::fs::Dir di samping API berbasis path yang sudah ada. Pengguna WASI Anda akan berterima kasih, dan pengguna Anda yang sadar keamanan akan lebih mudah melakukan sandbox terhadap aplikasi mereka.

Repository cap-std berada di github.com/bytecodealliance/cap-std. Dokumentasi crate mencakup catatan dukungan platform dan keseluruhan API surface. Mulailah dengan Dir::open_ambient_dir sebagai titik masuk, lalu coba hapus setiap pemanggilan lain ke std::fs yang menerima path absolut. Anda akan terkejut betapa banyak otoritas ambient yang dibawa-bawa oleh kode Anda.