Menyimpan Sandi
// SALAH — semuanya, tanpa kecuali
sandiHash = md5(sandi);
sandiHash = sha256(sandi);
sandiHash = sha256(sandi + garam);
// BENAR — fungsi yang memang dirancang untuk sandi
import argon2 from "argon2";
const hash = await argon2.hash(sandi, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MB
timeCost: 2,
parallelism: 1
});
const cocok = await argon2.verify(hash, sandiDimasukkan);
Fungsi hash umum dirancang secepat mungkin — itu keunggulannya untuk memeriksa integritas berkas. Untuk sandi, kecepatan itu bekerja untuk penyerang: satu GPU modern menghitung miliaran SHA-256 per detik, sehingga daftar sandi umum habis diuji dalam hitungan menit. Argon2 dan bcrypt sengaja dibuat lambat dan, pada Argon2, boros memori sehingga sulit dipercepat dengan GPU.
Perbandingan Waktu Tetap
// Rentan terhadap analisis waktu
if (tokenDiberikan === tokenAsli) { ... }
// Aman
import { timingSafeEqual } from "crypto";
const a = Buffer.from(tokenDiberikan);
const b = Buffer.from(tokenAsli);
if (a.length === b.length && timingSafeEqual(a, b)) { ... }
Perbandingan string biasa berhenti pada byte pertama yang berbeda. Selisih waktunya sangat kecil, tetapi dengan ribuan percobaan terukur, penyerang dapat menebak token karakter demi karakter. Pustaka verifikasi sandi seperti Argon2 sudah menangani ini secara internal.
Sesi: Cookie atau Token
Cookie sesi di server + dapat dicabut seketika + token tidak pernah terlihat JavaScript bila HttpOnly − perlu penyimpanan sesi bersama antar instans JWT tanpa keadaan + tidak perlu penyimpanan bersama − TIDAK dapat dicabut sebelum kedaluwarsa − payload terbaca siapa pun, hanya tanda tangannya yang terlindungi
Ketika pengguna keluar, mengganti sandi, atau akunnya dinonaktifkan, JWT yang sudah diterbitkan tetap berlaku sampai kedaluwarsa. Bila masa berlakunya 24 jam, akun yang dibobol tetap dapat diakses selama itu meski sandi sudah diganti. Pola yang lazim: token akses berumur sangat pendek (5–15 menit) ditambah token penyegar yang disimpan di server dan dapat dicabut.
Sebagian pustaka JWT lama menerima header alg: "none" dan melewati verifikasi tanda tangan sepenuhnya — penyerang cukup menyusun payload sendiri. Varian lain: token yang seharusnya RS256 dikirim sebagai HS256, lalu pustaka memakai kunci publik sebagai kunci rahasia HMAC. Selalu tetapkan algoritma yang diterima secara eksplisit saat verifikasi.
jwt.verify(token, kunci, { algorithms: ["RS256"] });
Pesan Galat yang Tidak Membocorkan
// Membocorkan surel mana yang terdaftar if (!pengguna) return "Surel tidak ditemukan"; if (!sandiCocok) return "Sandi salah"; // Aman return "Surel atau sandi salah";
Bila surel tidak ditemukan, kode sering langsung kembali tanpa menghitung hash — jauh lebih cepat daripada jalur dengan verifikasi Argon2. Selisih ratusan milidetik itu cukup untuk memetakan surel mana yang terdaftar. Perbaikannya: tetap lakukan verifikasi terhadap hash tiruan meski pengguna tidak ditemukan.
Alur Setel Ulang Sandi
1. Terima permintaan → SELALU balas "bila surel terdaftar, tautan dikirim" 2. Buat token acak kriptografis (32 byte), simpan HASH-nya saja 3. Masa berlaku pendek — 15 sampai 60 menit 4. Sekali pakai; hapus segera setelah dipakai 5. Setelah sandi diganti → cabut seluruh sesi yang ada 6. Kirim pemberitahuan ke surel bahwa sandi berubah
Bila basis data bocor dan token tersimpan apa adanya, penyerang dapat langsung memakai setiap token yang masih berlaku untuk mengambil alih akun. Menyimpan hash-nya membuat isi basis data tidak cukup untuk menyelesaikan alur — persis alasan sandi tidak disimpan apa adanya.
Membatasi Percobaan
Per akun 5 gagal → tunda bertambah, lalu kunci sementara Per IP 20 gagal per menit → tolak Global lonjakan tidak wajar → tinjau, mungkin credential stuffing
Pembatasan hanya per IP mudah dilewati dengan botnet, sementara pembatasan hanya per akun membuka pintu penguncian akun orang lain dengan sengaja. Keduanya diperlukan bersamaan.