OK, saya jelaskan: jika Anda memasukkan data pengguna, atau apapun yang berasal dari data pengguna ke dalam cookie untuk tujuan ini, Anda melakukan sesuatu yang salah.
Sana. Saya mengatakannya. Sekarang kita bisa beralih ke jawaban yang sebenarnya.
Apa yang salah dengan hashing data pengguna, Anda bertanya? Yah, itu turun ke permukaan paparan dan keamanan melalui ketidakjelasan.
Bayangkan sesaat bahwa Anda seorang penyerang. Anda melihat cookie kriptografi yang diset untuk mengingat-saya di sesi Anda. Lebar 32 karakter. Wah. Itu mungkin MD5 ...
Mari kita bayangkan juga bahwa mereka mengetahui algoritma yang Anda gunakan. Sebagai contoh:
md5(salt+username+ip+salt)
Sekarang, yang perlu dilakukan penyerang hanyalah memaksa "garam" (yang sebenarnya bukan garam, tetapi lebih dari itu nanti), dan dia sekarang dapat menghasilkan semua token palsu yang dia inginkan dengan nama pengguna apa pun untuk alamat IP-nya! Tapi memaksakan diri dengan garam itu sulit, bukan? Benar. Tetapi GPU modern sangat bagus dalam hal itu. Dan kecuali Anda menggunakan keacakan yang cukup di dalamnya (membuatnya cukup besar), itu akan jatuh dengan cepat, dan dengan itu kunci ke istananya.
Singkatnya, satu-satunya hal yang melindungi Anda adalah garam, yang tidak benar-benar melindungi Anda seperti yang Anda pikirkan.
Tapi tunggu!
Semua itu diprediksikan bahwa penyerang tahu algoritma! Jika ini rahasia dan membingungkan, maka Anda aman, bukan? SALAH . Garis pemikiran itu memiliki nama: Keamanan Melalui Ketidakjelasan , yang TIDAK PERNAH bisa diandalkan.
Cara yang Lebih Baik
Cara yang lebih baik adalah tidak pernah membiarkan informasi pengguna meninggalkan server, kecuali untuk id.
Saat pengguna masuk, buat token acak besar (128 hingga 256 bit). Tambahkan itu ke tabel database yang memetakan token ke userid, dan kemudian kirimkan ke klien dalam cookie.
Bagaimana jika penyerang menebak token acak dari pengguna lain?
Baiklah, mari kita berhitung di sini. Kami membuat token acak 128 bit. Itu artinya ada:
possibilities = 2^128
possibilities = 3.4 * 10^38
Sekarang, untuk menunjukkan seberapa besar angka itu, mari bayangkan setiap server di internet (katakanlah 50.000.000 hari ini) mencoba untuk memaksa jumlah itu dengan kecepatan masing-masing 1.000.000.000 per detik. Pada kenyataannya server Anda akan meleleh di bawah beban seperti itu, tetapi mari kita lakukan ini.
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
Jadi 50 kuadriliun tebakan per detik. Itu cepat! Baik?
time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000
Jadi 6,8 sextillion detik ...
Mari kita coba bawa ke nomor yang lebih ramah.
215,626,585,489,599 years
Atau lebih baik lagi:
47917 times the age of the universe
Ya, itu 47917 kali usia alam semesta ...
Pada dasarnya, itu tidak akan retak.
Jadi ringkasnya:
Pendekatan yang lebih baik yang saya sarankan adalah menyimpan cookie dengan tiga bagian.
function onLogin($user) {
$token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
storeTokenForUser($user, $token);
$cookie = $user . ':' . $token;
$mac = hash_hmac('sha256', $cookie, SECRET_KEY);
$cookie .= ':' . $mac;
setcookie('rememberme', $cookie);
}
Kemudian, untuk memvalidasi:
function rememberMe() {
$cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
if ($cookie) {
list ($user, $token, $mac) = explode(':', $cookie);
if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
return false;
}
$usertoken = fetchTokenByUserName($user);
if (hash_equals($usertoken, $token)) {
logUserIn($user);
}
}
}
Catatan: Jangan gunakan token atau kombinasi pengguna dan token untuk mencari catatan di database Anda. Selalu pastikan untuk mengambil catatan berdasarkan pengguna dan menggunakan fungsi perbandingan aman-waktu untuk membandingkan token yang diambil setelahnya. Lebih lanjut tentang serangan waktu .
Sekarang, sangat penting bahwa SECRET_KEY
rahasia kriptografi (dihasilkan oleh sesuatu seperti /dev/urandom
dan / atau berasal dari input entropi tinggi). Juga, GenerateRandomToken()
harus menjadi sumber acak yang kuat ( mt_rand()
tidak hampir cukup kuat. Gunakan perpustakaan, seperti RandomLib atau random_compat , atau mcrypt_create_iv()
dengan DEV_URANDOM
) ...
The hash_equals()
adalah untuk mencegah serangan waktu . Jika Anda menggunakan versi PHP di bawah PHP 5.6 fungsi hash_equals()
tidak didukung. Dalam hal ini Anda dapat mengganti hash_equals()
dengan fungsi timingSafeCompare:
/**
* A timing safe equals comparison
*
* To prevent leaking length information, it is important
* that user input is always used as the second parameter.
*
* @param string $safe The internal (safe) value to be checked
* @param string $user The user submitted (unsafe) value
*
* @return boolean True if the two strings are identical.
*/
function timingSafeCompare($safe, $user) {
if (function_exists('hash_equals')) {
return hash_equals($safe, $user); // PHP 5.6
}
// Prevent issues if string length is 0
$safe .= chr(0);
$user .= chr(0);
// mbstring.func_overload can make strlen() return invalid numbers
// when operating on raw binary strings; force an 8bit charset here:
if (function_exists('mb_strlen')) {
$safeLen = mb_strlen($safe, '8bit');
$userLen = mb_strlen($user, '8bit');
} else {
$safeLen = strlen($safe);
$userLen = strlen($user);
}
// Set the result to the difference between the lengths
$result = $safeLen - $userLen;
// Note that we ALWAYS iterate over the user-supplied length
// This is to prevent leaking length information
for ($i = 0; $i < $userLen; $i++) {
// Using % here is a trick to prevent notices
// It's safe, since if the lengths are different
// $result is already non-0
$result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}