Tamam, bunu açık bir şekilde ifade edeyim: kullanıcı verilerini veya kullanıcı verilerinden türetilen herhangi bir şeyi bu amaçla bir çereze koyarsanız, yanlış bir şey yapıyorsunuz.
Orada. Söyledim. Şimdi asıl cevaba geçebiliriz.
Kullanıcı verilerinin karmaşasında sorun nedir? Bu, belirsizlik yoluyla maruz kalma yüzeyine ve güvenliğe gelir.
Bir saniyeliğine saldırgan olduğunuzu düşünün. Oturumunuzda beni hatırla için ayarlanmış bir kriptografik çerez görüyorsunuz. 32 karakter genişliğindedir. Gee. Bu bir MD5 olabilir ...
Bir saniye için kullandığınız algoritmayı bildiğini düşünelim. Örneğin:
md5(salt+username+ip+salt)
Şimdi, bir saldırganın tek yapması gereken kaba kuvvet "tuz" (ki bu gerçekten bir tuz değil, daha sonra daha fazla) ve şimdi IP adresi için herhangi bir kullanıcı adıyla istediği tüm sahte belirteçleri üretebilir! Fakat kaba bir kaba zorlamak zor, değil mi? Kesinlikle. Ancak günümüzün GPU'ları son derece iyidir. Ve içinde yeterince rasgelelik kullanmadığınız sürece (yeterince büyük yapın), hızlı bir şekilde düşecek ve onunla kalenizin anahtarları olacaktır.
Kısacası, sizi koruyan tek şey, düşündüğünüz kadar sizi gerçekten koruyamayan tuzdur.
Fakat bekle!
Bütün bunlar saldırganın algoritmayı bildiği öngörülüyordu! Gizli ve kafa karıştırıcıysa, güvendesiniz değil mi? YANLIŞ . Bu düşüncenin bir adı vardır: ASLA güvenilmemesi gereken Müstehcenlikle Güvenlik .
Daha İyi Yol
Daha iyi bir yol, bir kullanıcı bilgilerinin kimlik dışında sunucudan ayrılmasına asla izin vermemektir.
Kullanıcı oturum açtığında büyük (128 ila 256 bit) rastgele bir belirteç oluşturun. Simgeyi kullanıcı kimliğiyle eşleyen bir veritabanı tablosuna ekleyin ve ardından tanımlama bilgisindeki istemciye gönderin.
Saldırgan başka bir kullanıcının rastgele jetonunu tahmin ederse ne olur?
Burada biraz matematik yapalım. 128 bit rastgele bir belirteç üretiyoruz. Bu şu demektir:
possibilities = 2^128
possibilities = 3.4 * 10^38
Şimdi, bu sayının ne kadar saçma olduğunu göstermek için, internetteki her sunucunun (bugün 50.000.000 diyelim), bu sayıyı saniyede 1.000.000.000 oranında kaba kuvvetle denemeye çalıştığını düşünelim. Gerçekte, sunucularınız böyle bir yük altında eriyecek, ancak hadi bunu oynayalım.
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
Saniyede 50 katrilyon tahmin. Hızlı! Sağ?
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
6.8 sextillion saniye ...
Bunu daha kolay sayılara indirmeye çalışalım.
215,626,585,489,599 years
Ya da daha iyisi:
47917 times the age of the universe
Evet, bu evrenin yaşının 47917 katı ...
Temel olarak, çatlamayacak.
Özetle:
Tavsiye ettiğim en iyi yaklaşım, çerezi üç parça ile saklamaktır.
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);
}
Ardından, doğrulamak için:
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);
}
}
}
Not: Veritabanınızdaki bir kaydı aramak için jetonu veya kullanıcı ve jeton kombinasyonunu kullanmayın. Her zaman kullanıcıya dayalı bir kayıt aldığınızdan ve getirilen belirteci daha sonra karşılaştırmak için zamanlama açısından güvenli bir karşılaştırma işlevi kullandığınızdan emin olun. Zamanlama saldırıları hakkında daha fazla bilgi .
Şimdi, kriptografik bir sır olması çok önemlidir SECRET_KEY
( /dev/urandom
yüksek entropi girdisine benzer ve / veya türetilmiş bir girdiden türetilmiştir). Ayrıca, GenerateRandomToken()
(kuvvetli rasgele kaynak olması gerekir mt_rand()
neredeyse yeterince güçlü değildir. Gibi kütüphane kullanma RandomLib veya random_compat veya mcrypt_create_iv()
birlikte DEV_URANDOM
) ...
hash_equals()
Önlemektir zamanlama saldırıları . PHP 5.6'nın altında bir PHP sürümü kullanıyorsanız işlev hash_equals()
desteklenmez. Bu durumda hash_equals()
, timingSafeCompare işleviyle değiştirebilirsiniz :
/**
* 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;
}