Nama berkas yang dikirim pengguna adalah input seperti biasa — dan input tidak tepercaya yang paling licin diolah program C++, karena kesalahannya menyentuh filesystem nyata. Celah paling klasik adalah path traversal: masukan seperti ../rahasia.txt meloloskan diri dari direktori yang seharusnya memagari akses. Chapter ini membangun validator path yang teruji dengan dua langkah — normalisasi leksikal dan pemeriksaan awalan — lalu menutup dengan dua jebakan yang biasanya menyertainya: TOCTOU dan symlink.
Path Traversal: Nama Berkas Tidak Boleh Dipercaya #
Program yang menerima berkas dari pengguna hampir selalu melakukan hal yang sama: menggabungkan direktori dasar dengan nama yang dikirim. Bila masukannya "laporan.txt" semua baik; bila masukannya "../rahasia.txt", jalur gabungan melompat keluar dari area yang ditentukan:
fs::path tujuan = "data" / masukanPengguna; // masukan = "../rahasia.txt"
// hasil: "data/../rahasia.txt" → saat dibuka, OS menyelesaikannya jadi "rahasia.txt"
Tidak ada yang salah secara sintaks — operator / milik std::filesystem memang bekerja begitu — dan justru itu bahayanya: kode terlihat rapi sementara batas area sudah tembus. Akibatnya program bisa membaca, menimpa, atau menghapus berkas apa pun yang izin prosesnya mencakup, bukan hanya yang ada di dalam data/. Karena itu aturannya tegas: nama berkas dari luar program divalidasi sebelum dipakai, di semua operasi — baca, tulis, dan penghapusan.
Validator Path: Normalisasi Leksikal dan Pemeriksaan Awalan #
Dua langkah menutup celah itu. Pertama, rapikan jalur gabungan dengan lexically_normal() — operasi murni teks pada komponen path yang menyelesaikan .., ., dan pemisah ganda tanpa menyentuh filesystem. Kedua, pastikan hasil normalisasi masih berawalan direktori dasar, diperiksa per komponen; bila tidak, tolak. Program berikut menguji empat masukan sekaligus — nama polos, nama yang hanya aman setelah normalisasi, traversal nyata, dan jalur yang lolos validasi tetapi berkasnya memang tidak ada:
// akses-berkas.cpp — hanya berkas di dalam direktori data/ yang boleh dibuka.
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
namespace fs = std::filesystem;
bool pathAman(const fs::path& dasar, const fs::path& masukan, fs::path& hasil) {
fs::path normal = (dasar / masukan).lexically_normal(); // rapikan "../", ".", "//"
auto [sisa, ujungDasar] =
std::mismatch(normal.begin(), normal.end(), dasar.begin(), dasar.end());
(void)sisa;
if (ujungDasar != dasar.end()) {
return false; // hasil normal tidak lagi berawalan dasar: di luar area
}
hasil = normal;
return true;
}
int main() {
fs::create_directories("data");
{
std::ofstream o("data/catatan.txt"); // berkas contoh dibuat sendiri, self-contained
o << "isi aman\n";
}
for (const std::string masukan : {"catatan.txt", "sub/../catatan.txt", "../rahasia.txt", "data/catatan.txt"}) {
fs::path aman;
if (!pathAman("data", masukan, aman)) {
std::cout << "ditolak: " << masukan << "\n";
continue;
}
std::ifstream berkas(aman); // buka langsung; kegagalan ditangani di sini
if (!berkas) {
std::cout << "gagal : " << masukan << " (tidak ada atau tidak bisa dibuka)\n";
continue;
}
std::string baris;
std::getline(berkas, baris);
std::cout << "dibaca : " << masukan << " -> " << baris << "\n";
}
return 0;
}Output yang Dihasilkan:
dibaca : catatan.txt -> isi aman
dibaca : sub/../catatan.txt -> isi aman
ditolak: ../rahasia.txt
gagal : data/catatan.txt (tidak ada atau tidak bisa dibuka)Bedah baris-per-baris:
- Baris 12 (normalisasi leksikal) —
(dasar / masukan).lexically_normal()merapikan komponen..,., dan//di lapisan teks:"data" / "sub/../catatan.txt"menjadidata/catatan.txt, sedangkan"data" / "../rahasia.txt"menjadirahasia.txt— pelarian langsung terlihat polos. - Baris 13–17 (pemeriksaan awalan per komponen) —
std::mismatchmembandingkan komponen path hasil normalisasi dengan komponen direktori dasar; biladasarhabis lebih dulu, hasil masih berada di dalam area. Pembandingannya per komponen, bukan per karakter:data-lama/x.txttidak akan lolos padahal berawalan huruf “data”. - Baris 24–28 (berkas uji dibuat sendiri) — program self-contained: membuat
data/dan menuliscatatan.txtlebih dulu, sehingga pengujian tidak bergantung pada berkas apa pun di luar. - Baris 30–35 (keputusan default: tolak) — masukan yang gagal pemeriksaan dicetak lalu dilompati; tidak ada jalur “coba buka saja”.
- Baris 36–39 (buka langsung, tangani kegagalan) —
std::ifstreammembuka hasil validasi; kegagalannya dibaca dari state stream dan dilaporkan, bukan dibiarkan diam-diam.
Empat baris output memeriksa empat watak masukan. catatan.txt aman sejak awal. sub/../catatan.txt hanya aman setelah normalisasi — persis kondisi yang dibiarkan lolos oleh kode naif. ../rahasia.txt tertangkap pemeriksaan awalan dan ditolak. data/catatan.txt justru lolos validasi: jalurnya menjadi data/data/catatan.txt, sah secara area, tetapi berkasnya memang tidak ada, sehingga program melaporkan kegagalan pembukaan secara terpisah. Validasi area dan keberadaan berkas adalah dua perhatian yang berbeda; keduanya butuh respons masing-masing.
Dua Jebakan Sisa: TOCTOU dan Symlink #
Pemeriksaan lalu buka adalah dua langkah berjarak waktu, dan di celah itu ada TOCTOU (time-of-check to time-of-use): antara validator menyatakan jalur aman dan ifstream membukanya, proses lain bisa mengganti berkas dengan symlink yang menunjuk ke luar area. Mitigasi ringkas yang sudah dipakai program di atas adalah memperkecil jeda itu: validasi lalu buka langsung, dan tangani error di titik buka — bukan mengandalkan hasil pemeriksaan yang sudah tua. Penerapan penuhnya memakai API sistem seperti open() dengan O_NOFOLLOW dan pemeriksaan pada file descriptor, yang keluar dari batas chapter ini.
Satu lagi: lexically_normal() bekerja pada teks path dan tidak mengikuti symlink — jalur yang tampak rapi bisa tetap menunjuk ke mana saja bila salah satu komponennya adalah symlink. Pemeriksaan yang lebih keras menyelesaikan jalur nyata dengan weakly_canonical() sebelum membandingkan awalan, ditambah pengaturan permission pada direktori dasar. Untuk aplikasi nyata, gabungkan keduanya: validasi leksikal untuk menolak input jahil lebih awal, resolusi kanonik untuk memastikan target akhirnya benar.
Dengan validator ini, satu sumber bahaya besar di filesystem sudah dipagari: nama berkas dari luar tak lagi bisa mengarahkan program ke mana saja. Bab berikutnya merangkum — kita telah melihat banyak wujud kerentanan satu per satu, dan sekarang saatnya menempatkan semuanya dalam kamus standar yang dipakai industri: CWE.
Referensi: cppreference — std::filesystem::path , cppreference — path::lexically_normal .