Operasi file adalah salah satu titik di mana program C berinteraksi dengan dunia luar, dan interaksi tersebut sering menjadi celah keamanan. Pada Chapter ini kita akan mempelajari tiga kelompok risiko utama: race condition TOCTOU saat mengecek dan kemudian membuka file, serangan path traversal yang menyusupkan ../ untuk keluar dari direktori yang seharusnya, dan masalah permission file yang terbuka terlalu longgar. Kita akan melihat pola yang aman untuk masing-masing kasus.

TOCTOU: Time-of-Check to Time-of-Use #

Kerentanan TOCTOU muncul ketika program memeriksa suatu kondisi file (misalnya “apakah file ini sudah ada?”) dan kemudian melakukan tindakan berdasarkan hasil cek tersebut. Di antara cek dan tindakan, penyerang bisa mengubah kondisi file, misalnya dengan menggantinya menjadi symlink ke file sensitif milik pengguna lain.

Pola yang rentan:

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

int rentan_toctou(const char *path) {
    // Cek: apakah file ada?
    if (access(path, F_OK) == 0) {
        printf("File sudah ada, lewati\n");
        return -1;
    }
    // Race window: penyerang bisa mengganti path dengan symlink di sini
    FILE *f = fopen(path, "w");
    if (!f) return -1;
    fprintf(f, "data\n");
    fclose(f);
    return 0;
}

Pola yang aman adalah membuka file langsung dengan flag yang sesuai dan memeriksa hasilnya, tanpa langkah cek terpisah. Gunakan open() dengan O_CREAT | O_EXCL untuk memastikan file baru dibuat (gagal jika sudah ada secara atomik):

c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int aman_create(const char *path) {
    // Buka dengan O_CREAT|O_EXCL: atomik, gagal jika file sudah ada
    int fd = open(path, O_CREAT | O_EXCL | O_WRONLY, 0600);
    if (fd < 0) {
        perror("open");
        return -1;
    }
    dprintf(fd, "data aman\n");
    close(fd);
    return 0;
}

int main(void) {
    aman_create("/tmp/test_toctou.txt");
    aman_create("/tmp/test_toctou.txt");  // Kedua kalinya akan gagal
    return 0;
}

Output yang Dihasilkan:

text
open: File exists

Pemanggilan kedua gagal karena O_EXCL mencegah pembuatan file yang sudah ada secara atomik. Tidak ada jendela waktu bagi penyerang untuk menukar file di antara cek dan pembuatan.

Path Traversal #

Path traversal terjadi ketika input pengguna mengandung karakter ../ yang membuat program mengakses file di luar direktori yang diizinkan. Misalnya, input ../../../../etc/passwd bisa membuat program membaca file sensitif.

Pola yang rentan:

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

void rentan_baca_log(const char *user_filename) {
    char path[256];
    snprintf(path, sizeof(path), "/var/log/app/%s", user_filename);
    FILE *f = fopen(path, "r");
    // Bisa membuka /var/log/app/../../etc/passwd
}

Solusi: validasi input dengan memeriksa tidak ada karakter .. atau path absolut, dan gunakan realpath() untuk mendapatkan path kanonik:

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

int aman_baca_file(const char *user_input) {
    // Tolak path absolut atau mengandung ..
    if (user_input[0] == '/' || strstr(user_input, "..") != NULL) {
        fprintf(stderr, "Path tidak valid\n");
        return -1;
    }

    char path[PATH_MAX];
    snprintf(path, sizeof(path), "/tmp/safe_dir/%s", user_input);

    // Dapatkan path kanonik (resolve semua symlink dan ..)
    char resolved[PATH_MAX];
    if (!realpath(path, resolved)) {
        perror("realpath");
        return -1;
    }

    // Pastikan path hasil resolve masih di dalam /tmp/safe_dir
    const char *base = "/tmp/safe_dir/";
    if (strncmp(resolved, base, strlen(base)) != 0) {
        fprintf(stderr, "Path di luar direktori yang diizinkan: %s\n", resolved);
        return -1;
    }

    FILE *f = fopen(resolved, "r");
    if (!f) {
        perror("fopen");
        return -1;
    }
    char buf[256];
    while (fgets(buf, sizeof(buf), f)) {
        printf("%s", buf);
    }
    fclose(f);
    return 0;
}

int main(void) {
    printf("Mencoba akses normal:\n");
    aman_baca_file("data.txt");
    printf("\nMencoba path traversal:\n");
    aman_baca_file("../../etc/passwd");
    return 0;
}

Output yang Dihasilkan:

text
Mencoba akses normal:

Mencoba path traversal:
Path tidak valid

Mari kita telaah baris-baris kode di atas:

  1. Baris 9 (if (user_input[0] == '/' ...): menolak path absolut atau yang mengandung .. sebelum dipakai membangun path lengkap.
  2. Baris 16 (realpath(path, resolved)): menyelesaikan semua symlink dan .. menjadi path absolut kanonik. Jika file tidak ada, realpath mengembalikan NULL.
  3. Baris 24 (strncmp(resolved, base, ...)): memastikan path hasil resolve masih diawali dengan base direktori yang diizinkan. Ini mencegah symlink yang lolos validasi pertama.

Permission File yang Aman #

Sika membuat file baru, selalu tentukan permission secara eksplisit. Permission default umask bisa membuat file readable oleh user lain.

c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(void) {
    // Permission 0600: hanya pemilik yang baca-tulis
    int fd = open("/tmp/secret_data.txt",
                  O_CREAT | O_WRONLY | O_TRUNC, 0600);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    dprintf(fd, "data rahasia\n");
    close(fd);
    printf("File dibuat dengan permission 0600\n");
    return 0;
}

Output yang Dihasilkan:

text
File dibuat dengan permission 0600

Verifikasi permission dari terminal:

bash
$ ls -la /tmp/secret_data.txt
-rw------- 1 user user 13 Oct 13 12:00 /tmp/secret_data.txt

Permission 0600 berarti hanya pemilik yang dapat membaca dan menulis. User lain tidak memiliki akses sama sekali.

Race Condition Saat Update File #

Saat program memperbarui file yang ada, pola read-modify-write bukan operasi atomik. Program lain bisa membaca file di tengah-tengah update dan mendapat data parsial atau inconsistent. Solusinya adalah pola atomic update: tulis ke file sementara, kemudian rename ke target. Operasi rename() bersifat atomik di POSIX.

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

int update_config_atomik(const char *target, const char *new_content) {
    char tmp_path[256];
    snprintf(tmp_path, sizeof(tmp_path), "%s.tmp.XXXXXX", target);

    int fd = mkstemp(tmp_path);
    if (fd < 0) {
        perror("mkstemp");
        return -1;
    }

    if (write(fd, new_content, strlen(new_content)) < 0) {
        perror("write");
        close(fd);
        unlink(tmp_path);
        return -1;
    }
    close(fd);

    if (rename(tmp_path, target) != 0) {
        perror("rename");
        unlink(tmp_path);
        return -1;
    }
    return 0;
}

int main(void) {
    update_config_atomik("/tmp/config.txt", "versi=2\naktif=true\n");
    printf("Update atomik berhasil\n");
    return 0;
}

Output yang Dihasilkan:

text
Update atomik berhasil

Pola ini menjamin bahwa pembaca yang lain hanya melihat versi lama atau versi baru, tidak pernah keadaan parsial di antaranya.