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 #
| Kategori | Item Utama |
|---|---|
| Input validation | Validasi panjang, range, dan format sebelum dipakai |
| Buffer management | Selalu pakai versi n (strncpy, snprintf, strncat) |
| Memory management | Cek return malloc, free setelah NULL, hindari realloc(p, 0) |
| Format string | Jangan pernah pakai input pengguna sebagai format string |
| Integer overflow | Cek INT_MAX - b sebelum a + b untuk signed |
| File path | Validasi path traversal (../), canonical path |
| Random number | Pakai /dev/urandom atau getrandom() untuk kripto, bukan rand() |
| Error handling | Cek return value setiap fungsi I/O dan alokasi |
| Concurrency | Hindari data race, pakai mutex atau atomics |
| Secret handling | Hapus 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.
| Berbahaya | Aman | Catatan |
|---|---|---|
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_s | strtok 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.
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:
/* 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.
/* 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:
gcc -std=c11 -Wall -Wextra -fanalyzer -o sec1 sec1.c
./sec1Output yang Dihasilkan:
== 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:
- Baris 11 (
sprintf(buf1, "Halo, %s", input)): Fungsisprintfmenulis tanpa batas kebuf1. Jikainputlebih panjang dari 58 karakter, terjadi overflow. GCC memberi warning-Wformat-overflow=saat compile, dan ASan menangkap saat runtime. - Baris 16 (
snprintf(buf2, sizeof(buf2), "Halo, %s", input)): Fungsisnprintfmenerima ukuran buffer dan tidak akan menulis lebih darisizeof(buf2)byte, termasuk null terminator. Output dipotong jika input terlalu panjang. - Baris 22-23 (
strncpy+ manual null):strncpymenyalin sampain-1byte, tetapi tidak menambah null jikasrclebih panjang. Barisbuf3[sizeof(buf3) - 1] = '\0'memastikan string selalu null-terminated. - Baris 30 (
strtok_r(str, ",", &save)): Versi reentrant daristrtok—menerima parametersaveuntuk menyimpan state antar pemanggilan. Aman dipakai di multi-thread. - Baris 38 (
strtol(s, &end, 10)): Mengembalikanlongdan menulis pointer ke karakter pertama yang tidak terbaca keend. Cekend == smendeteksi 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.
/* 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;
}gcc -std=c11 -Wall -Wextra -fsanitize=address -g -o sec_overflow sec_overflow.c
./sec_overflowOutput yang Dihasilkan (ASan menampilkan pesan error dan stack trace):
=================================================================
==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 vsprintfASan 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/reallocdicek return value-nya - Setiap
free(p)diikutip = NULL - Tidak ada
getsatauscanf("%s", buf)tanpa batas lebar - Format string
printfselalu berupa literal, bukan input pengguna - Input divalidasi panjang, range, dan format sebelum dipakai
- Path file divalidasi terhadap traversal (
../) - Random number kriptografis dari
/dev/urandomataugetrandom() - 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_satauexplicit_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)