Sejauh ini kita telah membangun aplikasi yang bekerja: melayani permintaan HTTP, menyimpan data ke database, dan mengenali pengguna lewat session serta token. Belum teruji adalah kondisi ketika ada pihak yang sengaja mencoba menyalahgunakan aplikasi tersebut — bukan lewat fitur yang kita sediakan, melainkan lewat celah yang tidak kita sadari telah terbuka. Section Security menghadapi kenyataan itu, dan sebelum menyentuh pertahanan yang spesifik, chapter ini menyiapkan empat prinsip dasar yang menjadi kerangka berpikir bagi seluruh chapter berikutnya. Prinsip-prinsip ini bukan teori hafalan; masing-masing akan kita lihat bekerja dalam kode Node.js yang nyata.

Empat Prinsip yang Menjadi Pondasi #

Prinsip keamanan adalah keputusan desain yang diambil sebelum kita menulis kode pertahanan apa pun. Ada empat yang akan muncul berulang kali di sepanjang Section ini:

  1. Jangan percaya masukan apa pun. Setiap byte yang datang dari luar — request body, query string, header, berkas yang diunggah, bahkan nilai dari database sendiri — dianggap tidak bersih sampai terbuktikan lolos pemeriksaan. Aplikasi yang menganggap “pengguna pasti mengisi form dengan benar” adalah aplikasi yang menyerahkan keputusan keamanannya kepada pengguna.
  2. Least privilege — beri hak akses seminimal mungkin. Akun database aplikasi tidak perlu bisa DROP TABLE, endpoint publik tidak perlu membaca folder sertifikat, dan proses server tidak perlu berjalan dengan akun administrator. Hak yang tidak pernah diberikan tidak bisa disalahgunakan.
  3. Defense in depth — pertahanan berlapis. Satu pemeriksaan bisa memiliki celah; dua pemeriksaan yang dirancang independen jarang rusak di titik yang sama. Validasi input, parameterized query, dan escape output bekerja di lapisan yang berbeda, sehingga kegagalan satu lapisan tidak langsung menjadi kebocoran data.
  4. Fail securely — saat terjadi kegagalan, sistem jatuh ke kondisi yang aman. Timeout ke server izin, data konfigurasi yang hilang, atau format masukan yang tak terduga semuanya harus bermuara pada penolakan akses, bukan pada akses yang tidak sengaja terbuka.

Kalimat singkatnya: tolak secara default, izinkan secara eksplisit. Pola pikir ini membalik kebiasaan pemula yang menulis kode seolah semua masukan bersih lalu menambahkan pemeriksaan di sana-sini. Pemetaan berikut merangkum keempat prinsip ke dalam praktik konkret di Node.js:

PrinsipWujud konkret di Node.js
Jangan percaya masukanFungsi validasi di setiap pintu masuk data (Chapter Validasi dan Sanitasi Input)
Least privilegeAllowlist peran, akun database terbatas, flag node --permission
Defense in depthValidasi + parameterized query + escape output bekerja berlapis
Fail securelyDefault TOLAK pada gerbang akses; aplikasi menolak menyala tanpa konfigurasi

Least Privilege Sampai ke Proses Itu Sendiri #

Least privilege sering dianggap urusan administrator sistem, padahal prinsip ini bekerja di tiga lapisan yang semuanya menyentuh pekerjaan kita. Lapisan pertama adalah kode: object HAK_AKSES pada demo di bawah memetakan sumber daya ke daftar peran yang diizinkan — keanggotaan eksplisit, bukan pengecualian. Lapisan kedua adalah akun layanan: koneksi database yang dipakai aplikasi sebaiknya hanya memiliki hak SELECT, INSERT, dan UPDATE pada tabel yang memang disentuh, bukan hak administratif penuh. Lapisan ketiga adalah proses Node.js itu sendiri.

Node.js menyediakan penerapan least privilege di level proses melalui Permission Model. Sejak versi 20 dan kini berstatus stabil, flag node --permission mematikan semua akses berbahaya secara default; proses hanya bisa membaca berkas atau membuka koneksi jaringan pada jalur yang kita izinkan lewat flag seperti --allow-fs-read. Mode ini menyesuaikan semangat prinsip kita: izin diberikan satu per satu, secara eksplisit. Detail penggunaannya dibahas pada Section Modern Node.js; yang penting di sini adalah kesadaran bahwa batas hak bisa dituntut sampai ke proses runtime.

Fail Securely: Keputusan Default Adalah Tolak #

Prinsip fail securely paling mudah diperiksa lewat pertanyaan sederhana: apa yang terjadi pada kode ini bila masukannya kosong, rusak, atau tidak dikenal? Mari kita terjemahkan ke gerbang akses yang keputusannya selalu jatuh ke TOLAK kecuali semua pemeriksaan lolos:

js
// Demo prinsip "fail securely" dan "least privilege":
// gerbang akses yang keputusan default-nya adalah TOLAK.
const HAK_AKSES = {
  'laporan-penjualan': ['manajer'],
  'data-pengguna': ['admin'],
};

function putuskanAkses(pengguna, sumberDaya) {
  // Setiap kondisi tak terduga jatuh ke keputusan TOLAK.
  if (pengguna === null || typeof pengguna !== 'object') return 'TOLAK';
  const peran = pengguna.peran;
  if (typeof peran !== 'string') return 'TOLAK';
  const diizinkan = HAK_AKSES[sumberDaya];
  if (diizinkan === undefined) return 'TOLAK';
  return diizinkan.includes(peran) ? 'IZINKAN' : 'TOLAK';
}

const uji = [
  [{ nama: 'Budi', peran: 'manajer' }, 'laporan-penjualan'],
  [{ nama: 'Siti', peran: 'staf' }, 'laporan-penjualan'],
  [{ nama: 'Dewi', peran: 'admin' }, 'menu-tidak-terdaftar'],
  [null, 'data-pengguna'],
  [{ nama: 'Rian' }, 'data-pengguna'],
];

for (const [pengguna, sumberDaya] of uji) {
  const nama = pengguna?.nama ?? 'tanpa-nama';
  console.log(
    `${nama.padEnd(11)} -> ${sumberDaya.padEnd(21)} : ${putuskanAkses(pengguna, sumberDaya)}`
  );
}

Output yang Dihasilkan:

text
Budi        -> laporan-penjualan     : IZINKAN
Siti        -> laporan-penjualan     : TOLAK
Dewi        -> menu-tidak-terdaftar  : TOLAK
tanpa-nama  -> data-pengguna         : TOLAK
Rian        -> data-pengguna         : TOLAK

Mari kita bedah struktur gerbang tersebut:

  1. Baris 3-6 (HAK_AKSES): satu-satunya sumber kebenaran tentang siapa boleh mengakses apa. Menambah sumber daya baru berarti menambah satu baris di sini — hak akses tidak pernah tersebar di dalam handler.
  2. Baris 10 (pengguna === null ...): masukan null atau bertipe lain langsung ditolak sebelum kode sempat membaca properti apa pun. Tanpa baris ini, null.peran akan melempar TypeError — dan error yang tidak tertangani di gerbang akses adalah bentuk kegagalan yang tidak terkontrol.
  3. Baris 11-12 (typeof peran): object tanpa properti peran juga ditolak. Pemeriksaan tipe diletakkan sebelum pemakaian nilai, bukan sesudahnya.
  4. Baris 13-14 (diizinkan === undefined): sumber daya yang tidak terdaftar ditolak. Ini pilihan yang disengaja — kebalikannya (mengizinkan hal yang belum diketahui) berarti setiap typo pada nama sumber daya justru membuka akses baru.
  5. Baris 15 (includes(peran)): satu-satunya jalur menuju IZINKAN. Baca kembali seluruh function — setiap cabang selain baris ini bernilai TOLAK, itulah artinya tolak secara default.
  6. Baris 18-31 (data uji): lima skenario mencakup kasus normal (Budi), peran yang memang tidak berhak (Siti), sumber daya tak dikenal (Dewi), masukan null, dan object tanpa peran (Rian). Empat dari lima skenario berakhir TOLAK — persis perilaku yang kita inginkan dari sebuah gerbang.

Defense in Depth dengan Lapisan Independen #

Pertahanan berlapis hanya berguna bila lapisan-lapisannya benar-benar independen: memakai mekanisme yang berbeda untuk memeriksa hal yang berbeda. Contoh klasiknya adalah fungsi transfer dana berikut, yang memeriksa bentuk data dan aturan bisnis sebagai dua lapisan terpisah:

js
function transferAman(pengguna, jumlah) {
  // Lapisan 1: bentuk data.
  if (Number.isInteger(jumlah) === false || jumlah <= 0 || jumlah > 1_000_000) {
    return { status: 'TOLAK', alasan: 'jumlah tidak valid' };
  }
  // Lapisan 2: aturan bisnis.
  if (pengguna.saldo < jumlah) {
    return { status: 'TOLAK', alasan: 'saldo tidak cukup' };
  }
  return { status: 'PROSES', alasan: 'seluruh lapisan lolos' };
}

console.log(JSON.stringify(transferAman({ saldo: 500_000 }, 2_000_000)));
console.log(JSON.stringify(transferAman({ saldo: 500_000 }, 50_000)));
console.log(JSON.stringify(transferAman({ saldo: 500_000 }, '50_000')));

Output yang Dihasilkan:

text
{"status":"TOLAK","alasan":"jumlah tidak valid"}
{"status":"PROSES","alasan":"seluruh lapisan lolos"}
{"status":"TOLAK","alasan":"jumlah tidak valid"}

Skenario pertama menembus lapisan bentuk data (angka bulat, positif, di bawah batas) lalu ditahan lapisan saldo. Skenario kedua lolos kedua lapisan. Skenario ketiga — jumlah dikirim sebagai string — tertahan di lapisan pertama. Bayangkan lapisan saldo memiliki bug yang keliru melewatkan angka negatif: lapisan bentuk data tetap menahannya, karena jumlah <= 0 diperiksa oleh pemeriksaan yang tidak bergantung pada logika saldo. Independensi semacam ini yang membuat kebocoran satu lapisan tidak otomatis menjadi kebocoran data.

Dengan kerangka berpikir yang terpasang, kita siap membangun lapisan pertahanan pertama — dan paling sering dilupakan — dari aplikasi Node.js: memeriksa serta membersihkan setiap data yang masuk. Itulah materi Chapter Validasi dan Sanitasi Input. Pranala berguna untuk pendalaman: panduan keamanan web di MDN dan pusat belajar Node.js , serta dokumentasi Permission Model untuk least privilege di level proses.