Di antara kerentanan C++, format string injection punya rasio dampak-ke-baris-kode yang mencengangkan: cukup satu argumen yang salah posisi untuk membuka pembacaan memori sembarangan. Celahnya muncul saat teks dari luar program dipakai sebagai string format printf — bukan sebagai data. Program kemudian mengartikan simbol % milik pengguna sebagai perintah baca/tulis yang tidak pernah kita tulis. Chapter ini menjelaskan mekanismenya tanpa menjalankan payload apa pun, menunjukkan pola aman yang teruji, dan menjelaskan mengapa iostream serta std::format tidak mewarisi masalah ini.
Mekanisme: Argumen yang Tidak Ada #
printf adalah fungsi variadik: setelah parameter pertama (string format), ia membaca argumen tambahan sesuai permintaan simbol % di dalam format. Pemisahan ini murni konvensi runtime — printf tidak tahu berapa argumen yang benar-benar dikirim pemanggil. Bila formatnya berasal dari luar, jumlah dan jenis permintaan % juga ditentukan dari luar:
std::printf(nama); // nama = "Budi %s %s %s" → printf minta tiga argumen
Ketika nama berisi %s, printf mengambil argumen berikutnya dari tempat argumen seharusnya berada — register dan stack. Karena tidak ada argumen yang dikirim, yang terbaca adalah sampah di lokasi itu, lalu sampah itu dibaca sebagai pointer dan didereferensi untuk dicetak. Akibatnya bisa crash, bisa bocor isi memori proses ke output, dan dengan simbol %n — yang menulis jumlah karakter tercetak ke pointer argumen — bisa menjadi penulisan memori yang alamatnya dipilih penyerang. Bahkan input yang tak bersalah seperti "disk 100% penuh" berbahaya: % p adalah konversi yang tidak sah dan perilaku printf-nya tidak terdefinisi.
Yang membuat celah ini licin: printf(nama) dan printf("%s", nama) sama-sama mengkompilasi tanpa suara pada input bertipe char*. GCC 14 menyertakan -Wformat-security (aktif di -Wall) yang menandai bentuk paling nyata, tetapi peringatan itu tidak menggantikan kebiasaan yang benar.
Pola Aman yang Teruji #
Aturannya satu kalimat: teks format selalu literal yang kita tulis, input selalu jadi argumen. Program berikut menerima input yang sengaja berisi simbol %s, %n, dan %p — payload favorit — lalu memperlakukannya murni sebagai data:
// sapa-aman.cpp — input pengguna hanya sebagai argumen, tidak pernah sebagai format.
#include <cstdio>
#include <format>
#include <iostream>
#include <string>
int main() {
std::string nama;
std::cout << "masukkan nama: ";
std::getline(std::cin, nama);
if (nama.empty()) {
nama = "(tanpa nama)";
}
// Pola aman 1 (warisan C): format = string literal, input = argumen.
std::printf("Halo, %s! Poin: %d\n", nama.c_str(), 100);
// Pola aman 2 (C++): std::format memisahkan teks dan data, tipe diperiksa compiler.
std::cout << std::format("Halo, {}! Poin: {}\n", nama, 100);
return 0;
}Output yang Dihasilkan:
masukkan nama: Halo, Budi %s %n %p! Poin: 100
Halo, Budi %s %n %p! Poin: 100Bedah baris-per-baris:
- Baris 10–12 (
std::getline) — input dibaca utuh daristd::cin, termasuk simbol%apa pun; tidak ada interpretasi pada tahap ini. - Baris 17 (
std::printf("Halo, %s! Poin: %d\n", nama.c_str(), 100)) — format adalah string literal yang kita kendalikan;namaberperan sebagai argumen yang diisi ke%s. Simbol%di dalamnamadicetak apa adanya karena tidak pernah dibaca sebagai format. - Baris 20 (
std::format("Halo, {}! Poin: {}\n", nama, 100)) — padanan C++: teks dan data terpisah, placeholder{}hanya menyisipkan argumen, danstd::formatmenolak bila jumlah argumen tidak cocok.
Perhatikan input di output: persis "Budi %s %n %p", dicetak sebagai teks biasa. Dengan pola printf(nama), input yang sama bisa jadi bencana; dengan pola format-literal, payload itu cuma deretan karakter.
Mengapa iostream dan std::format Aman #
std::cout << nama tidak punya masalah ini karena argumen dan operasinya terpisah oleh tipe: operator << menerima satu objek tertentu, bukan teks yang diinterpretasi ulang. std::format membawa jaminan satu langkah lebih jauh — argumennya diperiksa saat kompilasi. Format literal yang meminta placeholder lebih banyak daripada argumen yang dikirim, atau memakai konversi yang tidak cocok dengan tipe argumen, ditolak compiler sebelum program jalan. std::print("...{}", x) mewarisi pemeriksaan yang sama.
Rangkumannya bisa dibaca sebagai tabel keputusan:
| Cara keluaran | Format dari input berbahaya | Format literal |
|---|---|---|
printf(fmt) | Kerentanan: baca/tulis memori | Aman, tetapi tetap cek argumen |
printf("%s", input) | Aman (input sebagai argumen) | Aman — pola C yang benar |
cout << input | Aman, tipe terpisah | Aman |
std::format(fmt, input) | Aman; tipe/urutan diperiksa compiler bila fmt literal | Aman |
Keluaran yang aman hanyalah satu sisi gerbang data dari luar program. Sisi yang lebih luas — memvalidasi bentuk, panjang, dan rentang setiap input sebelum dipakai — adalah topik chapter berikutnya.
Referensi: cppreference — std::printf , cppreference — std::format .