Setiap file yang dibuat program dapat membocorkan data bila permission-nya terlalu longgar. CWE-732 (Incorrect Permission Assignment for Critical Resource) mencatat kasus di mana file log, file konfigurasi sementara, atau cache ditulis dengan mode default 0644—dapat dibaca semua pengguna di sistem. Pada chapter ini kita mempraktikkan dua pola penanganan file yang aman: pengaturan permission eksplisit 0600 saat open(), dan pemakaian mkstemp() sebagai pengganti tmpnam() yang sudah deprecated karena rentan TOCTOU.

Tiga Aturan Dasar Penanganan File Aman #

  1. Permission eksplisit: Selalu teruskan mode saat open(). Jangan mengandalkan umask default.
  2. Nama file tidak predictable: File sementara harus memiliki nama acak—mkstemp, bukan tmpnam atau tmpfile dengan nama hard-coded.
  3. Atomik: Gabungkan O_CREAT | O_EXCL untuk mencegah symlink race. Lihat chapter Race Conditions untuk detail.
c
/* secure_file_handling.c */
/* Izin file 0600, mkstemp() sebagai pengganti tmpnam(). */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

/* AMAN: mkstemp membuat file unik dengan mode 0600 secara atomik. */
static int demo_mkstemp(void) {
    /* Template harus berakhir dengan 6 'X'. */
    char tmpl[] = "/tmp/test_12a_secure_XXXXXX";
    int fd = mkstemp(tmpl);
    if (fd < 0) {
        fprintf(stderr, "[ERROR] mkstemp gagal: %s\n", strerror(errno));
        return -1;
    }
    printf("(mkstemp) file dibuat: %s (fd=%d)\n", tmpl, fd);

    const char *data = "isi file sementara\n";
    ssize_t wrote = write(fd, data, strlen(data));
    if (wrote < 0) {
        fprintf(stderr, "[ERROR] write gagal: %s\n", strerror(errno));
    }
    close(fd);

    /* Cek permission. */
    struct stat st;
    if (stat(tmpl, &st) == 0) {
        printf("(mkstemp) permission = %04o (diharapkan 0600)\n",
               st.st_mode & 0777);
    }
    unlink(tmpl);
    return 0;
}

/* AMAN: buka file dengan izin 0600 secara eksplisit. */
static int demo_open_with_mode(void) {
    const char *path = "/tmp/test_12a_mode_demo.txt";
    int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0600);
    if (fd < 0) {
        fprintf(stderr, "[ERROR] open gagal: %s\n", strerror(errno));
        return -1;
    }
    const char *secret = "data rahasia\n";
    write(fd, secret, strlen(secret));
    close(fd);
    printf("[open] file dibuat: %s (mode 0600)\n", path);

    struct stat st;
    if (stat(path, &st) == 0) {
        printf("[open] permission = %04o (diharapkan 0600)\n",
               st.st_mode & 0777);
    }
    unlink(path);
    return 0;
}

/* VULNERABLE (demo): tmpnam() — sudah deprecated, mudah di-race. */
static void demo_tmpnam_danger(void) {
    /* Hanya tampilkan pesan, jangan benar-benar pakai tmpnam(). */
    printf("[tmpnam] DEPRECATED: gunakan mkstemp() sebagai gantinya\n");
}

int main(void) {
    printf("=== 1. mkstemp (rekomendasi) ===\n");
    demo_mkstemp();

    printf("\n=== 2. open() dengan mode 0600 ===\n");
    demo_open_with_mode();

    printf("\n=== 3. tmpnam (jangan dipakai) ===\n");
    demo_tmpnam_danger();
    return 0;
}

Output yang Dihasilkan:

text
=== 1. mkstemp (rekomendasi) ===
(mkstemp) file dibuat: /tmp/test_12a_secure_zcSggM (fd=3)
(mkstemp) permission = 0600 (diharapkan 0600)

=== 2. open() dengan mode 0600 ===
[open] file dibuat: /tmp/test_12a_mode_demo.txt (mode 0600)
[open] permission = 0600 (diharapkan 0600)

=== 3. tmpnam (jangan dipakai) ===
[tmpnam] DEPRECATED: gunakan mkstemp() sebagai gantinya

Mari kita telaah baris-baris kode di atas:

  1. Baris 13-31 (demo_mkstemp): mkstemp menggantikan enam karakter XXXXXX di akhir template dengan kombinasi acak, kemudian membuka file dengan flag O_CREAT | O_EXCL | O_WRONLY dan mode 0600 dalam satu panggilan. Operasi ini atomik—tidak ada jendela TOCTOU. File descriptor yang dikembalikan langsung siap dipakai. Setelah selesai, kita close(fd) dan unlink(tmpl) agar file hilang dari filesystem (data tetap ada di disk sampai ditimpa—untuk keamanan data sensitif, hapus dengan shred atau timpa dengan nol sebelum unlink).
  2. Baris 34-48 (demo_open_with_mode): Pola untuk file yang bukan sementara—misalnya file konfigurasi pengguna. Argumen 0600 diteruskan ke open(). Permission final juga dipengaruhi umask proses—0600 & ~umask. Untuk memastikan mode persis 0600, panggil umask(0) sebelum open atau fchmod(fd, 0600) setelahnya.
  3. Baris 51-54 (demo_tmpnam_danger): Pesan pengingat. tmpnam menghasilkan nama file sementara yang predictable—penyerang dapat menebaknya dan membuat symlink lebih dulu. tmpnam dihapus di C23.
  4. Output (mkstemp): Permission 0600 dikonfirmasi oleh stat. File hanya dapat dibaca/ditulis oleh pemilik. Pengguna lain di sistem tidak dapat mengaksesnya bahkan bila nama file berhasil ditebak.
  5. Output (open): Permission 0600 juga dikonfirmasi. Bila kita lupa menambah argumen mode, open(O_CREAT | O_WRONLY, 0) akan membuat file dengan permission 0 & ~umask—biasanya 0000 yang berarti file tidak dapat diakses oleh siapapun termasuk pemilik.

Memastikan Mode Presisi dengan fchmod #

umask proses dapat mengubah mode yang sebenarnya diterapkan open(). Untuk memastikan mode presisi 0600 terlepas dari umask, panggil fchmod(fd, 0600) setelah open berhasil:

c
/* extra_secure_file_handling.c — fchmod untuk memastikan mode presisi. */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

int main(void) {
    const char *path = "/tmp/test_12a_fchmod_demo.txt";
    int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0600);
    if (fd < 0) {
        fprintf(stderr, "open gagal: %s\n", strerror(errno));
        return 1;
    }
    /* fchmod menjamin mode presisi terlepas dari umask. */
    if (fchmod(fd, 0600) != 0) {
        fprintf(stderr, "fchmod gagal: %s\n", strerror(errno));
        close(fd);
        return 1;
    }
    const char *msg = "data dengan permission presisi 0600\n";
    write(fd, msg, strlen(msg));
    close(fd);

    struct stat st;
    if (stat(path, &st) == 0) {
        printf("File: %s\n", path);
        printf("Mode: %04o (diharapkan 0600)\n", st.st_mode & 0777);
    }
    unlink(path);
    return 0;
}

Output yang Dihasilkan:

text
File: /tmp/test_12a_fchmod_demo.txt
Mode: 0600 (diharapkan 0600)

fchmod bekerja pada file descriptor yang sudah terbuka—tidak ada jendela TOCTOU antara open dan chmod seperti yang terjadi pada chmod(path, 0600) yang menerima nama file. Pendekatan ini direkomendasikan oleh SEI CERT C aturan FIO01-C.

Mengapa open() Lebih Aman daripada fopen() #

fopen(path, "w") memang lebih ringkas, tetapi tidak menerima argumen mode. File dibuat dengan mode default 0666 & ~umask—biasanya 0644—yang berarti dapat dibaca semua pengguna. Untuk file data sensitif, gunakan open() dengan mode eksplisit lalu fdopen(fd, "w") untuk membungkus fd menjadi FILE * bila perlu API stdio:

c
int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0600);
if (fd < 0) { /* error */ }
FILE *f = fdopen(fd, "w");
if (f == NULL) { close(fd); /* error */ }
fprintf(f, "data rahasia: %s\n", secret);
fclose(f);  /* juga menutup fd */

Daftar Bandingkan Fungsi File #

FungsiAtomic?PermissionRekomendasi
fopen(path, "w")Tidak (race)Default 0644Hindari untuk data sensitif
open(path, O_CREAT|O_EXCL, 0600)YaEksplisit 0600Rekomendasi
mkstemp(tmpl)Ya0600Rekomendasi untuk file sementara
tmpnam() + fopen()TidakDefault 0644Dilarang—dihapus di C23
tmpfile()Ya0600 (implisit)OK bila tidak perlu nama file
mkdtemp(tmpl)Ya0700Rekomendasi untuk direktori sementara

Penghapusan Aman untuk Data Sensitif #

unlink hanya menghapus nama file dari direktori—data masih ada di inode sampai semua fd ditutup dan tidak ada referensi lain. Bahkan setelah itu, data tetap ada di blok disk sampai ditimpa. Untuk file yang berisi kredensial atau kunci privat, hapus dengan overwrite sebelum unlink:

c
int fd = open(path, O_WRONLY);
struct stat st;
fstat(fd, &st);
/* Timpa dengan nol — sederhana, tetapi SSD dengan wear-leveling
   mungkin tidak benar-benar menimpa. Untuk data sangat sensitif,
   gunakan fsync dan pertimbangkan enkripsi penuh disk. */
off_t len = st.st_size;
char zero[4096] = {0};
while (len > 0) {
    ssize_t n = (len < (off_t)sizeof zero) ? len : (off_t)sizeof zero;
    write(fd, zero, n);
    len -= n;
}
fsync(fd);
close(fd);
unlink(path);

File Descriptor Leakage dan O_CLOEXEC #

Pada program yang memanggil execve, file descriptor yang terbuka dapat bocor ke proses anak—bila fd tidak ditutup, proses anak dapat membaca atau menulis file tersebut. Untuk mencegahnya, tambahkan O_CLOEXEC saat open:

c
int fd = open(path, O_CREAT | O_WRONLY | O_CLOEXEC, 0600);

Flag ini menjamin fd otomatis tertutup saat execve dipanggil—tidak perlu loop fcntl(fd, F_SETFD, FD_CLOEXEC) manual setelahnya (yang juga memiliki jendela TOCTOU antara open dan fcntl).