Keamanan kode C bukan fitur tambahan yang dilampirkan di akhir pengembangan, melainkan keputusan desain yang harus hadir sejak baris pertama. Bahasa C memberi programmer kendali langsung terhadap memori, pointer, dan panggilan sistem—kekuatan yang sama membuat C ringan juga membuatnya rentan bila digunakan tanpa disiplin. Pada chapter ini kita menetapkan fondasi yang sama-sama dipakai oleh OWASP dan SEI CERT C Coding Standard, yaitu tiga prinsip inti: least privilege, defense in depth, dan fail secure. Kita akan melihat ketiganya bekerja dalam satu modul autentikasi sederhana.

Tiga Prinsip Dasar Secure Coding #

Least privilege menyatakan bahwa setiap modul, proses, atau pengguna hanya boleh memiliki hak akses minimum yang diperlukan untuk menyelesaikan tugasnya. Sebuah program server web tidak perlu berjalan sebagai root hanya karena perlu membuka port 80—kita dapat memakai capability bounding atau setuid yang sempit. Defense in depth menempatkan beberapa lapisan validasi independen sehingga kegagalan satu lapisan tidak langsung membuka pintu. Fail secure menentukan bahwa bila terjadi keadaan tak terduga (input NULL, alokasi gagal, koneksi putus), program harus default ke kondisi yang paling aman—biasanya menolak aksi, bukan mengabulkan.

c
/* secure_c_fundamentals.c — demo least privilege, defense in depth, fail secure. */
#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/types.h>

#define MAX_USER 32
#define MAX_PASS 64

/* Lapisan 1: validasi panjang input. */
static int validate_length(const char *s, size_t max_len) {
    if (s == NULL) return 0;
    size_t len = strlen(s);
    return len > 0 && len <= max_len;
}

/* Lapisan 2: cek karakter (hanya alfanumerik). */
static int validate_chars(const char *s) {
    if (s == NULL) return 0;
    for (const char *p = s; *p != '\0'; p++) {
        if (!( (*p >= 'a' && *p <= 'z') ||
               (*p >= 'A' && *p <= 'Z') ||
               (*p >= '0' && *p <= '9') )) {
            return 0;
        }
    }
    return 1;
}

/* Lapisan 3: cek kredensial (dummy check untuk demo). */
static int check_credentials(const char *user, const char *pass) {
    /* FAIL SECURE: bila ada input NULL, langsung ditolak. */
    if (user == NULL || pass == NULL) return 0;
    if (strcmp(user, "admin") == 0 && strcmp(pass, "s3cret") == 0) {
        return 1;
    }
    return 0;
}

/* Fungsi login menggabungkan ketiga lapisan (defense in depth). */
static int login(const char *user, const char *pass) {
    if (!validate_length(user, MAX_USER)) return 0;
    if (!validate_length(pass, MAX_PASS)) return 0;
    if (!validate_chars(user))            return 0;
    if (!validate_chars(pass))            return 0;
    return check_credentials(user, pass);
}

/* Laporkan privilege saat ini (bukan drop privilege — kita tidak punya root). */
static void report_privilege(void) {
    uid_t ruid = getuid();
    uid_t euid = geteuid();
    printf("Real UID      : %u\n", ruid);
    printf("Effective UID : %u\n", euid);
    printf("Jalankan sebagai root? %s\n",
           (euid == 0) ? "YA (tidak disarankan)" : "TIDAK (least privilege)");
}

int main(void) {
    report_privilege();
    printf("\n");

    const char *users[] = { "admin", "admin", "guest", NULL, "admin" };
    const char *passes[] = { "s3cret", "wrong", "s3cret", "s3cret", "s3cret;rm -rf /" };
    const size_t n = sizeof(users) / sizeof(users[0]);

    for (size_t i = 0; i < n; i++) {
        int ok = login(users[i], passes[i]);
        printf("Test %zu: user='%s' pass='%s' -> %s\n",
               i + 1,
               users[i] ? users[i] : "(null)",
               passes[i] ? passes[i] : "(null)",
               ok ? "LULUS" : "DITOLAK");
    }
    return 0;
}

Output yang Dihasilkan:

text
Real UID      : 1001
Effective UID : 1001
Jalankan sebagai root? TIDAK (least privilege)

Test 1: user='admin' pass='s3cret' -> LULUS
Test 2: user='admin' pass='wrong' -> DITOLAK
Test 3: user='guest' pass='s3cret' -> DITOLAK
Test 4: user='(null)' pass='s3cret' -> DITOLAK
Test 5: user='admin' pass='s3cret;rm -rf /' -> DITOLAK

Mari kita telaah baris-baris kode di atas:

  1. Baris 12-15 (validate_length): Lapisan pertama defense in depth. Fungsi ini menolak input NULL, string kosong, dan string yang lebih panjang dari max_len. Karena lapisan ini berdiri sendiri, kegagalan pada fungsi parsing berikutnya tidak akan membiarkan input panjang lolos begitu saja.
  2. Baris 18-28 (validate_chars): Lapisan kedua memeriksa karakter satu per satu. Pendekatan ini juga mencegah injection karakter seperti ; yang dipakai pada Test 5 untuk mencoba command chaining. Karakter ; bukan alfanumerik, sehingga password ditolak sebelum pernah dibandingkan dengan kredensial sebenarnya.
  3. Baris 31-37 (check_credentials): Lapisan terdalam. Bahkan di sini kita tetap mengulang pemeriksaan NULL. Prinsip fail secure membuat fungsi mengembalikan 0 (tolak) ketika ada input tak terduga, bukan 1 (izinkan).
  4. Baris 40-46 (login): Orkestrasi ketiga lapisan secara berurutan. Setiap lapisan bersifat independen—bug pada validate_length tidak otomatis melemahkan validate_chars.
  5. Baris 49-55 (report_privilege): Demonstrasi least privilege. Program melaporkan effective UID-nya dan secara eksplisit mengonfirmasi bahwa program tidak berjalan sebagai root. Di produksi, sebuah service sebaiknya melepas privilege root setelah membuka port di bawah 1024 melalui setuid(non_privileged_uid).

Hubungan dengan Standar Industri #

Tiga prinsip di atas dipakai konsisten oleh dua standar besar:

  • OWASP memetakan defense in depth sebagai salah satu kontrol utama dalam OWASP ASVS (Application Security Verification Standard).
  • SEI CERT C Coding Standard menempatkan fail secure pada aturan ERR33-C—Detect errors when calling a function—yang mewajibkan penanganan kode balik dari setiap fungsi standar yang dapat gagal.

Demonstrasi Penurunan Privilege (Least Privilege) #

Pada program yang membutuhkan privilege root untuk membuka port < 1024 atau membaca file tertentu, privilege sebaiknya dilepas segera setelah operasi awal selesai. Berikut demo setuid() yang aman dijalankan sebagai pengguna biasa:

c
/* extra_secure_c_fundamentals.c — demonstrasi penurunan privilege. */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
    uid_t ruid = getuid();
    uid_t euid = geteuid();

    printf("Sebelum drop: ruid=%u euid=%u\n", ruid, euid);

    /* Simulasi drop privilege: setuid hanya berhasil bila euid==0. */
    if (euid == 0) {
        if (setuid(ruid) != 0) {
            perror("setuid gagal");
            return 1;
        }
        printf("Setelah drop : ruid=%u euid=%u (privilege diturunkan)\n",
               getuid(), geteuid());
    } else {
        printf("Tidak berjalan sebagai root — tidak ada privilege untuk di-drop.\n");
        printf("Least privilege tetap terpenuhi: euid != 0.\n");
    }
    return 0;
}

Output yang Dihasilkan:

text
Sebelum drop: ruid=1001 euid=1001
Tidak berjalan sebagai root — tidak ada privilege untuk di-drop.
Least privilege tetap terpenuhi: euid != 0.

Bila program berjalan sebagai root, setuid(ruid) mengganti effective UID ke UID pengguna yang menjalankan binary SUID—setelah panggilan ini, program tidak lagi memiliki privilege root meskipun ada bug berikutnya.

Mengapa Tiga Prinsip Ini Cukup sebagai Titik Mulai #

Banyak kerentanan yang akan kita bahas di chapter berikutnya—buffer overflow, format string, integer overflow—dapat diredam oleh penerapan disiplin ini. Input validation di boundary adalah bentuk defense in depth. Pemeriksaan batas sebelum operasi aritmetika adalah bentuk fail secure. Penurunan privilege sebelum memproses data pengguna adalah bentuk least privilege. Dengan menguasai tiga prinsip ini, kita siap membedah kerentanan spesifik dengan kerangka berpikir yang konsisten.