Buffer overflow adalah kerentanan tertua yang masih hidup: program menyalin data ke buffer tetap tanpa memeriksa ukuran, data melebihi kapasitas, dan byte kelebihannya menimpa area tetangga di memori. Bila buffer berada di stack, byte yang tertimpa bisa saja alamat kembali (return address), dan itulah pintu klasik eksekusi kode dari luar. Karena itu berinteraksi dengan data dari luar program — nama berkas, kolom CSV, baris request — menggunakan buffer mentah adalah keputusan yang harus dihindari. Chapter ini membedah mekanismenya, memetakan fungsi C yang berbahaya versus lebih aman, lalu menunjukkan bagaimana C++ modern menyingkirkan masalahnya sama sekali.

Bagaimana Overflow Terjadi #

Semua variabel lokal bertumpuk di stack frame fungsi, berdampingan dengan data pembukuan seperti alamat kembali. Sebuah char nama[8] menempati tepat 8 byte; tidak ada pagar di sebelahnya. Ketika program menyalin sembilan byte ke dalamnya, byte kesembilan ditulis ke frame di luar nama — tanpa error, tanpa peringatan runtime, tanpa apa pun. Di heap ceritanya mirip: blok yang terlalu panjang menimpa metadata allocator atau blok tetangga, dan kerusakan baru terlihat saat alokasi berikutnya.

Konsekuensinya berlapis. Pada lapis paling ringan, data yang bertetangga berubah dan program berperilaku aneh. Pada lapis terberat, penyerang yang mengendalikan data yang disalin (misalnya isi dari input) bisa menyusun byte kelebihan sehingga mengarahkan eksekusi program. Karena itulah buffer overflow menjadi salah satu kategori CWE paling diperbincangkan, seperti akan kita lihat pada chapter CWE.

Fungsi C Berbahaya versus Lebih Aman #

Warisan C memberi C++ sekeluarga fungsi string yang tidak menghitung kapasitas. Memetakan mana yang dilarang dan mana yang masih bisa dipakai adalah keterampilan wajib saat membaca kode lama:

FungsiPerilakuStatus
getsTanpa batas sama sekali; mustahil dipakai dengan amanDihapus dari standar C11; dilarang
strcpy, strcatMenyalin sampai NUL, tidak bertanya kapasitasHindari; pilih alternatif berbatas
sprintfMenulis tanpa batas ke bufferHindari; pakai snprintf
strncpyBerbatas, tetapi boleh tanpa NUL akhir bila sumber lebih panjangHati-hati; NUL harus dipasang manual
snprintfBerbatas dan selalu menutup dengan NUL; mengembalikan panjang yang dibutuhkanPilihan utama bila buffer C wajib ada

Kasus strncpy sering mengecoh: ia memotong, tetapi bila tujuan penuh, hasilnya tidak berakhir NUL — pembacaan string itu kemudian melampaui batas lagi. Itulah sebabnya pemeriksaan panjang dan penutupan NUL tetap tanggung jawab kita meski fungsi “berbatas” dipakai.

Ketika compiler bisa melihat ukurannya, ia ikut menjaga. Program berikut menyalin string 9 byte (termasuk NUL) ke buffer 8 byte:

cpp strcpy-rentan.cpp — strcpy ke buffer tetap (dijalankan di sandbox, tidak dipaketkan)
#include <cstring>
#include <iostream>

int main() {
    char nama[8];
    std::strcpy(nama, "Prakarsa");   // 8 karakter + NUL = 9 byte, muat 8
    std::cout << nama << "\n";
    return 0;
}

Kompilasi biasa sudah memperingatkan, dan menjalankannya di bawah AddressSanitizer menghentikan program pada penulisan yang menyeberangi batas:

text Peringatan compiler dan laporan ASan (disingkat; alamat berbeda di mesin Anda)
strcpy-rentan.cpp:6:16: warning: 'void* __builtin_memcpy(void*, const void*,
long unsigned int)' writing 9 bytes into a region of size 8 overflows the
destination [-Wstringop-overflow=]

==6963==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fda7b700028 ...
WRITE of size 9 at 0x7fda7b700028 thread T0
    #1 0x55b261790227 in main strcpy-rentan.cpp:6
SUMMARY: AddressSanitizer: stack-buffer-overflow strcpy-rentan.cpp:6 in main

Peringatan -Wstringop-overflow= hanya muncul karena panjang sumber terlihat saat kompilasi. Begitu sumbernya berupa data runtime — isi berkas, argumen program — compiler kehilangan piringan hitungnya, dan satu-satunya pengawas tinggal kita.

Versi Aman: std::string dan snprintf #

C++ modern mengganti buffer mentah dengan std::string yang tumbuh sendiri; buffer C tetap ada hanya di sambungan dengan API lama, dan di sana snprintf membatasi penulisan sekaligus melaporkan kebutuhan sesungguhnya:

cpp
// teks-aman.cpp — std::string untuk teks; snprintf bila antarmuka C menuntut buffer tetap.

#include <cstdio>
#include <iostream>
#include <string>

int main() {
    std::string nama = "Budi";
    std::string salam = "Halo, " + nama + "!";   // tumbuh sendiri, tidak ada batas tetap
    std::cout << salam << "\n";

    std::string pengguna = "Siti Pratama";       // panjang dari luar: tidak bisa diprediksi
    char buffer[16];
    int butuh = std::snprintf(buffer, sizeof buffer, "Halo, %s!", pengguna.c_str());
    std::cout << "butuh         = " << butuh << " karakter\n";
    std::cout << "buffer        = \"" << buffer << "\"\n";
    std::cout << "ukuran buffer = " << sizeof buffer << " byte (termasuk '\\0')\n";
    return 0;
}

Output yang Dihasilkan:

text
Halo, Budi!
butuh         = 19 karakter
buffer        = "Halo, Siti Prat"
ukuran buffer = 16 byte (termasuk '\0')

Bedah baris-per-baris:

  1. Baris 7 (std::string salam = ...) — penyambungan string dialokasikan std::string sesuai kebutuhan; tidak ada kapasitas tetap yang bisa dilampaui, sehingga kelas bug ini hilang dari akar.
  2. Baris 10 (std::string pengguna = "Siti Pratama";) — variabel ini berperan sebagai data dari luar: panjangnya tidak bisa dipastikan saat penulisan kode, persis seperti input pengguna.
  3. Baris 12 (std::snprintf(buffer, sizeof buffer, ...)) — snprintf menerima kapasitas buffer dan dijamin menutup hasil dengan NUL; string yang lebih panjang dipotong, bukan meluap.
  4. Baris 13 (int butuh = ...) — nilai kembali berisi panjang yang dibutuhkan (19) bila buffer tak terbatas. Angka ini memang sebaiknya diperiksa: bila butuh >= sizeof buffer, berarti terpotong dan keputusan bisa diambil dengan sadar (tolak, ulangi dengan buffer lebih besar, atau catat).

Modern C++: Bagaimana Bila Tetap Butuh Buffer? #

Untuk data biner berukuran tetap, std::array<T, N> menyimpan ukurannya di tipe sehingga melampauinya menjadi kelihatan; std::span lalu mengoper data bersama panjangnya tanpa menyalin. Satu perangkap tersisa: std::string_view dan data() dari std::string tidak menjamin NUL di akhir — operasi apa pun yang menuntut string C membutuhkan c_str() atau salinan eksplisit. Dengan tiga aturan ini — std::string untuk teks, span/array untuk data, snprintf untuk sambungan C — buffer overflow bisa dijadikan bug yang tidak pernah tertulis.

Kerentanan berikutnya bekerja pada lapisan lain: bukan memori yang terlampaui, melainkan angka yang melampaui rentangnya — dan bagaimana luapan itu dipakai untuk menembus perhitungan ukuran dan indeks.

Referensi: cppreference — std::snprintf , cppreference — std::basic_string .