Bahasa C mempercayakan banyak tanggung jawab keamanan ke programmer—tidak ada bounds checking otomatis, tidak ada garbage collector, tidak ada type safety runtime. Satu kesalahan sprintf dapat menyebabkan eksekusi kode arbitrary. Bab ini menyusun checklist praktik aman yang sebaiknya kita ikuti setiap kali menulis kode C produksi. Daftar ini diorganisir per kategori—input validation, manajemen memori, format string, dan kriptografi.

Checklist Kategori #

KategoriItem Utama
Input validationValidasi panjang, range, dan format sebelum dipakai
Buffer managementSelalu pakai versi n (strncpy, snprintf, strncat)
Memory managementCek return malloc, free setelah NULL, hindari realloc(p, 0)
Format stringJangan pernah pakai input pengguna sebagai format string
Integer overflowCek INT_MAX - b sebelum a + b untuk signed
File pathValidasi path traversal (../), canonical path
Random numberPakai /dev/urandom atau getrandom() untuk kripto, bukan rand()
Error handlingCek return value setiap fungsi I/O dan alokasi
ConcurrencyHindari data race, pakai mutex atau atomics
Secret handlingHapus secret dari memori segera setelah dipakai

Buffer Management #

Aturan utama: setiap operasi yang menulis ke buffer harus tahu ukuran buffer tersebut. Hindari fungsi yang tidak menerima parameter ukuran.

BerbahayaAmanCatatan
strcpy(dst, src)strncpy(dst, src, n); dst[n-1]='\0'Atau snprintf(dst, n, "%s", src)
strcat(dst, src)strncat(dst, src, n)Pastikan n menghitung ruang tersisa
sprintf(buf, ...)snprintf(buf, sizeof(buf), ...)Selalu
gets(buf)fgets(buf, sizeof(buf), stdin)gets dihapus di C11
strtok(s, d)strtok_r(s, d, &save) atau strtok_sstrtok pakai static state, tidak aman thread
scanf("%s", buf)scanf("%63s", buf) (size-1)Atau fgets lalu parse

Input Validation #

Setiap input dari luar (pengguna, file, network) harus divalidasi sebelum dipakai. Validasi mencakup panjang, range numerik, format, dan karakter terlarang.

c
static int parse_age(const char *s) {
    char *end;
    long val = strtol(s, &end, 10);
    /* Cek apakah ada konversi sama sekali */
    if (end == s) return -1;
    /* Cek apakah ada karakter sisa setelah angka */
    if (*end != '\0') return -1;
    /* Cek range */
    if (val < 0 || val > 150) return -1;
    return (int)val;
}

Format String Vulnerability #

Serangan format string terjadi saat input pengguna dipakai sebagai format string printf:

c
/* BERBAHAYA */
printf(user_input);          /* jika user_input = "%x %x %x", bocor stack */
sprintf(buf, user_input);    /* sama, plus overflow */

/* AMAN */
printf("%s", user_input);
snprintf(buf, sizeof(buf), "%s", user_input);

Penyerang dapat menyisipkan %x untuk membaca stack, %n untuk menulis ke alamat memori. Selalu gunakan string literal sebagai format, dengan input pengguna sebagai argumen.

Program Demo #

Program berikut membandingkan versi tidak aman dan aman dari operasi string, demonstrasi strtok_r yang reentrant, dan validasi input dengan strtol.

c
/* File: sec1.c - Demonstrasi konsep secure C */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define BUFSZ 64

static void demo_unsafe_vs_safe(const char *input) {
    /* TIDAK AMAN: sprintf tanpa batas (overflow bila input > 58 char) */
    char buf1[BUFSZ];
    sprintf(buf1, "Halo, %s", input);
    printf("[unsafe sprintf]  %s\n", buf1);

    /* AMAN: snprintf dengan batas ukuran */
    char buf2[BUFSZ];
    snprintf(buf2, sizeof(buf2), "Halo, %s", input);
    printf("[safe   snprintf] %s\n", buf2);

    /* AMAN: strncpy dengan null terminator eksplisit */
    char buf3[BUFSZ];
    strncpy(buf3, input, sizeof(buf3) - 1);
    buf3[sizeof(buf3) - 1] = '\0';
    printf("[safe   strncpy]  %s\n", buf3);
}

static void demo_strtok_r(void) {
    char str[] = "satu,dua,tiga";   /* mutable! bukan literal */
    char *save;
    char *tok = strtok_r(str, ",", &save);
    while (tok) {
        printf("token: %s\n", tok);
        tok = strtok_r(NULL, ",", &save);
    }
}

static void demo_input_validation(const char *s) {
    char *end;
    long val = strtol(s, &end, 10);
    if (end == s || *end != '\0') {
        printf("input '%s' tidak valid (bukan integer)\n", s);
    } else if (val < 0 || val > 100) {
        printf("input %ld di luar rentang [0, 100]\n", val);
    } else {
        printf("input valid: %ld\n", val);
    }
}

int main(void) {
    printf("== Demo sprintf vs snprintf (input pendek, aman) ==\n");
    demo_unsafe_vs_safe("Budi");

    printf("\n== Demo strtok_r (reentrant) ==\n");
    demo_strtok_r();

    printf("\n== Demo validasi input strtol ==\n");
    demo_input_validation("42");
    demo_input_validation("abc");
    demo_input_validation("999");

    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -fanalyzer -o sec1 sec1.c
./sec1

Output yang Dihasilkan:

text
== Demo sprintf vs snprintf (input pendek, aman) ==
[unsafe sprintf]  Halo, Budi
[safe   snprintf] Halo, Budi
[safe   strncpy]  Budi

== Demo strtok_r (reentrant) ==
token: satu
token: dua
token: tiga

== Demo validasi input strtol ==
input valid: 42
input 'abc' tidak valid (bukan integer)
input 999 di luar rentang [0, 100]

Mari kita telaah baris-baris kode di atas:

  1. Baris 11 (sprintf(buf1, "Halo, %s", input)): Fungsi sprintf menulis tanpa batas ke buf1. Jika input lebih panjang dari 58 karakter, terjadi overflow. GCC memberi warning -Wformat-overflow= saat compile, dan ASan menangkap saat runtime.
  2. Baris 16 (snprintf(buf2, sizeof(buf2), "Halo, %s", input)): Fungsi snprintf menerima ukuran buffer dan tidak akan menulis lebih dari sizeof(buf2) byte, termasuk null terminator. Output dipotong jika input terlalu panjang.
  3. Baris 22-23 (strncpy + manual null): strncpy menyalin sampai n-1 byte, tetapi tidak menambah null jika src lebih panjang. Baris buf3[sizeof(buf3) - 1] = '\0' memastikan string selalu null-terminated.
  4. Baris 30 (strtok_r(str, ",", &save)): Versi reentrant dari strtok—menerima parameter save untuk menyimpan state antar pemanggilan. Aman dipakai di multi-thread.
  5. Baris 38 (strtol(s, &end, 10)): Mengembalikan long dan menulis pointer ke karakter pertama yang tidak terbaca ke end. Cek end == s mendeteksi input kosong/non-angka; *end != '\0' mendeteksi sisa karakter setelah angka.

Demonstrasi Buffer Overflow dengan ASan #

Program berikut sengaja memuat bug sprintf overflow. Compile dengan AddressSanitizer untuk melihat error runtime.

c
/* File: sec_overflow.c - Buffer overflow yang ditangkap ASan */
#include <stdio.h>
#define BUFSZ 16

int main(void) {
    char buf[BUFSZ];
    const char *input = "input_sangat_panjang_yang_akan_meluap_buffer";
    sprintf(buf, "Halo, %s", input);
    printf("buf = %s\n", buf);
    return 0;
}
bash
gcc -std=c11 -Wall -Wextra -fsanitize=address -g -o sec_overflow sec_overflow.c
./sec_overflow

Output yang Dihasilkan (ASan menampilkan pesan error dan stack trace):

text
=================================================================
==31920==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fe3e6100030 at pc 0x7fe3e864be00 bp 0x7fff63760cb0 sp 0x7fff63760470
WRITE of size 51 at 0x7fe3e6100030 thread T0
    #0 0x7fe3e864bdff in vsprintf
    #1 0x7fe3e864d3c1 in sprintf
    #2 0x5562c2d84236 in main /tmp/cc/test/sec_overflow.c:8
    ...
SUMMARY: AddressSanitizer: stack-buffer-overflow in vsprintf

ASan mendeteksi WRITE of size 51 ke buffer buf yang hanya berukuran 16 byte—overflow tepat 35 byte. Tanpa ASan, program mungkin crash dengan segfault acak atau, lebih buruk, berjalan normal tetapi memberi celah eksploitasi.

Checklist Akhir Sebelum Rilis #

  • Semua operasi string pakai versi n (strncpy, snprintf, strncat)
  • Semua malloc/calloc/realloc dicek return value-nya
  • Setiap free(p) diikuti p = NULL
  • Tidak ada gets atau scanf("%s", buf) tanpa batas lebar
  • Format string printf selalu berupa literal, bukan input pengguna
  • Input divalidasi panjang, range, dan format sebelum dipakai
  • Path file divalidasi terhadap traversal (../)
  • Random number kriptografis dari /dev/urandom atau getrandom()
  • Kode di-compile dengan -Wall -Wextra -Werror -fanalyzer
  • Test dijalankan dengan -fsanitize=address,undefined
  • Tidak ada secret (password, key) hard-coded—baca dari env atau file config
  • Secret dihapus dari memori (memset_s atau explicit_bzero) setelah dipakai
  • Bounds checking dilakukan pada setiap akses array yang menerima indeks dari luar
  • Fungsi I/O file/network selalu cek return value
  • Error message ke pengguna tidak membocorkan struktur internal (path file, alamat memori)