Pemrosesan string adalah salah satu sumber kerentanan keamanan paling sering dijumpai pada program C. Buffer overflow—kondisi di mana fungsi menulis byte melebihi batas buffer yang dialokasikan—dapat menyebabkan crash, kebocoran data, hingga eksekusi kode arbitrer oleh penyerang. Banyak fungsi string library klasik (strcpy, strcat, sprintf, gets, scanf("%s")) tidak menerima parameter ukuran buffer dan dengan demikian rentan terhadap overflow. Pada chapter penutup ini, kita akan mendemonstrasikan buffer overflow secara konkret, kemudian memperbaikinya dengan tiga lapis pertahanan: (1) fungsi bounded seperti strncpy dan strncat; (2) snprintf untuk formatting dengan batas; (3) pengecekan panjang input sebelum pemrosesan.

Demonstrasi Buffer Overflow #

Mulai dari contoh berbahaya: strcpy ke buffer yang lebih kecil dari sumber. Tanpa proteksi compiler, program akan menulis melebihi batas buffer dan merusak memori di sebelahnya.

c
#include <stdio.h>
#include <string.h>

/* Demo buffer overflow: strcpy tanpa cek ukuran */
int main(void) {
    char buf_kecil[8];
    char input_besar[] = "Pemrograman C yang menyenangkan";   /* panjang 32 */

    /* Dengan -FORTIFY_SOURCE, gcc akan mendeteksi overflow saat runtime */
    /* Tanpa proteksi: kode ini akan menimpa memori setelah buf_kecil */
    strcpy(buf_kecil, input_besar);
    printf("buf_kecil = %s\n", buf_kecil);
    printf("Anda mungkin tidak sampai sini bila FORTIFY_SOURCE aktif.\n");
    return 0;
}

Output yang Dihasilkan (program di-compile tanpa _FORTIFY_SOURCE):

text
Segmentation fault (exit code 139)

Pada contoh di atas, buf_kecil berkapasitas 8 byte, tetapi input_besar berisi 32 karakter plus null terminator (total 33 byte). strcpy menyalin seluruh 33 byte ke buf_kecil, menulis 25 byte melebihi batas. Byte-byte tersebut menimpa memori di sebelah buf_kecil—kemungkinan return address di stack frame main. Saat main hendak return, CPU melompat ke alamat yang rusak dan kernel mengirim SIGSEGV. Program berakhir dengan exit code 139 (= 128 + 11, di mana 11 adalah nomor sinyal SIGSEGV).

snprintf: Formatting Aman #

snprintf(buf, size, fmt, ...) adalah versi aman dari sprintf. Fungsi ini menulis maksimal size - 1 karakter lalu menambahkan null terminator. Yang istimewa: snprintf selalu mengembalikan jumlah karakter yang akan ditulis tanpa potongan—sehingga pemanggil dapat mendeteksi apakah truncation terjadi.

c
#include <stdio.h>
#include <string.h>

/* Pola aman dengan snprintf: tidak pernah menulis melebihi ukuran buffer */
int main(void) {
    char buf[8];
    const char *input = "Pemrograman C yang menyenangkan";

    /* snprintf menulis maksimal sizeof buf - 1 karakter lalu tambah '\0' */
    int ditulis = snprintf(buf, sizeof buf, "%s", input);

    printf("Buf: %s\n", buf);
    printf("Jumlah byte yang akan ditulis (tanpa potong): %d\n", ditulis);
    printf("Jumlah byte yang benar-benar tersimpan: %zu\n", strlen(buf));

    /* snprintf selalu mengembalikan panjang yang AKAN ditulis tanpa dipotong.
       Kita bisa pakai untuk cek apakah truncation terjadi. */
    if ((size_t)ditulis >= sizeof buf) {
        printf("PERINGATAN: input terpotong (butuh buffer %d byte).\n",
               ditulis + 1);
    }
    return 0;
}

Output yang Dihasilkan:

text
Buf: Pemrogr
Jumlah byte yang akan ditulis (tanpa potong): 31
Jumlah byte yang benar-benar tersimpan: 7
PERINGATAN: input terpotong (butuh buffer 32 byte).

Mari kita telaah baris-baris kode di atas:

  1. Baris 8 (char buf[8];): buffer tujuan hanya 8 byte.
  2. Baris 12 (int ditulis = snprintf(buf, sizeof buf, "%s", input);): snprintf menulis maksimal sizeof buf - 1 = 7 karakter ("Pemrogr") lalu menambahkan null terminator. Buffer tidak overflow. Yang penting: return value ditulis = 31 adalah panjang yang akan ditulis tanpa potongan, bukan yang sebenarnya tertulis.
  3. Baris 14 (printf("Buf: %s\n", buf);): buffer berisi "Pemrogr" (7 karakter) — bukti truncation.
  4. Baris 15-16: kita mencetak keduanya—panjang yang akan ditulis (31) dan panjang aktual yang tersimpan (7). Selisih antara dua nilai ini menandakan truncation.
  5. Baris 20-23 (if ((size_t)ditulis >= sizeof buf) { ... }): bila return value ≥ ukuran buffer, truncation terjadi. Pemanggil dapat memilih untuk membatalkan operasi, mengalokasikan buffer yang lebih besar, atau hanya mencatat peringatan. Inilah mekanisme deteksi truncation yang elegan dari snprintf.

Membangun Path dengan Aman #

Mari melihat pola yang menggabungkan beberapa string menjadi satu dengan batas yang aman. Kasus klasik: membangun file path dari direktori dan nama file.

c
#include <stdio.h>
#include <string.h>

/* Gabungkan beberapa string dengan aman menggunakan snprintf */
void buat_path(char *dst, size_t ukuran,
               const char *dir, const char *file) {
    snprintf(dst, ukuran, "%s/%s", dir, file);
}

int main(void) {
    char path[32];
    buat_path(path, sizeof path, "/var/log", "aplikasi.log");
    printf("Path: %s (panjang %zu)\n", path, strlen(path));

    /* Path yang lebih panjang dari buffer akan terpotong */
    buat_path(path, sizeof path, "/var/log", "aplikasi_sangat_panjang_nama_file.txt");
    printf("Path terpotong: %s\n", path);
    printf("Tidak ada crash, hanya terpotong.\n");

    /* strncat sebagai alternatif aman untuk concatenation */
    char buf[16] = "ID:";
    strncat(buf, "2024-USER-001", sizeof buf - strlen(buf) - 1);
    printf("ID: %s\n", buf);
    return 0;
}

Output yang Dihasilkan:

text
Path: /var/log/aplikasi.log (panjang 21)
Path terpotong: /var/log/aplikasi_sangat_panjan
Tidak ada crash, hanya terpotong.
ID: ID:2024-USER-00

Mari kita telaah baris-baris kode di atas:

  1. Baris 5-7 (buat_path): fungsi pembungkus snprintf dengan format "%s/%s". Argumen ukuran dibawa sampai ke snprintf sehingga tidak ada overflow terlepas dari panjang dir atau file.
  2. Baris 11-12 (path pertama): "/var/log" + "/" + "aplikasi.log" = "/var/log/aplikasi.log" (21 karakter). Muat di buffer 32 byte—tidak ada truncation.
  3. Baris 16-17 (path kedua, terpotong): "/var/log" + "/" + "aplikasi_sangat_panjang_nama_file.txt" = 47 karakter—melebihi 32 byte. snprintf memotong menjadi 31 karakter ("/var/log/aplikasi_sangat_panjan") + null terminator di byte ke-32. Tidak ada crash, hanya truncation.
  4. Baris 22-23 (strncat): kita menggabungkan "ID:" (3 karakter) dengan "2024-USER-001" (13 karakter). Total: 16 karakter + null terminator = 17 byte—melebihi kapasitas buf[16]. Argumen ketiga strncat = sizeof buf - strlen(buf) - 1 = 16 - 3 - 1 = 12. Artinya strncat hanya menambahkan maksimal 12 karakter dari sumber, lalu null terminator. Hasil: "ID:2024-USER-00" (15 karakter + null terminator = 16 byte penuh). Truncation terjadi dengan aman.

Ringkasan Konsep #

Keamanan pemrosesan string di C bermuara pada satu prinsip: selalu hormati batas buffer. Fungsi klasik tanpa parameter ukuran (strcpy, strcat, sprintf, gets, scanf("%s")) rentan overflow dan sebaiknya dihindari. Gunakan padanan bounded: strncpy + forced null terminator, strncat dengan perhitungan ruang tersisa, snprintf dengan deteksi truncation lewat return value, fgets untuk pembacaan input. Untuk validasi ketat, periksa panjang input dengan strlen sebelum pemrosesan dan alokasikan buffer dengan ukuran panjang + 1 (untuk null terminator). Buffer overflow bukan sekadar crash—ia adalah vektor serangan keamanan klasik yang masih relevan hingga hari ini. Dengan disiplin memilih fungsi yang benar dan pengecekan yang konsisten, kita dapat menulis kode C yang robust dan tahan terhadap input adversarial. Sampai jumpa di section berikutnya dari C course ini.