OK, hãy để tôi nói thẳng điều này: nếu bạn đưa dữ liệu người dùng hoặc bất cứ thứ gì có nguồn gốc từ dữ liệu người dùng vào cookie cho mục đích này, bạn đã làm sai điều gì đó.
Đó Tôi nói rồi. Bây giờ chúng ta có thể chuyển sang câu trả lời thực tế.
Có gì sai với băm dữ liệu người dùng, bạn yêu cầu? Vâng, nó đi xuống bề mặt tiếp xúc và bảo mật thông qua che khuất.
Hãy tưởng tượng trong một giây rằng bạn là kẻ tấn công. Bạn thấy một cookie mật mã được đặt cho bộ nhớ trong phiên của bạn. Nó rộng 32 ký tự. Trời ạ. Đó có thể là MD5 ...
Chúng ta cũng hãy tưởng tượng trong một giây rằng họ biết thuật toán mà bạn đã sử dụng. Ví dụ:
md5(salt+username+ip+salt)
Bây giờ, tất cả những kẻ tấn công cần làm là vũ phu "muối" (không thực sự là muối, nhưng nhiều hơn về sau), và giờ anh ta có thể tạo ra tất cả các mã thông báo giả mà anh ta muốn với bất kỳ tên người dùng nào cho địa chỉ IP của mình! Nhưng vũ phu - ép muối là khó, phải không? Chắc chắn rồi. Nhưng GPU hiện đại thì cực kỳ tốt. Và trừ khi bạn sử dụng đủ tính ngẫu nhiên trong nó (làm cho nó đủ lớn), nó sẽ nhanh chóng rơi xuống, và với nó là chìa khóa cho lâu đài của bạn.
Nói tóm lại, thứ duy nhất bảo vệ bạn là muối, thứ không thực sự bảo vệ bạn nhiều như bạn nghĩ.
Nhưng đợi đã!
Tất cả điều đó đã được chứng minh rằng kẻ tấn công biết thuật toán! Nếu nó bí mật và khó hiểu, thì bạn có an toàn không? SAI . Dòng suy nghĩ đó có một cái tên: Bảo mật thông qua che khuất , không bao giờ nên dựa vào.
Cách tốt hơn
Cách tốt hơn là không bao giờ để thông tin của người dùng rời khỏi máy chủ, ngoại trừ id.
Khi người dùng đăng nhập, tạo mã thông báo ngẫu nhiên lớn (128 đến 256 bit). Thêm vào bảng cơ sở dữ liệu để ánh xạ mã thông báo đến userid, sau đó gửi nó đến máy khách trong cookie.
Điều gì xảy ra nếu kẻ tấn công đoán mã thông báo ngẫu nhiên của người dùng khác?
Chà, hãy làm một số phép toán ở đây. Chúng tôi đang tạo mã thông báo ngẫu nhiên 128 bit. Điều đó có nghĩa là có:
possibilities = 2^128
possibilities = 3.4 * 10^38
Bây giờ, để cho thấy con số đó lớn đến mức nào, hãy tưởng tượng mọi máy chủ trên internet (giả sử 50.000.000 ngày nay) đang cố gắng ép buộc con số đó với tốc độ 1.000.000.000 mỗi giây. Trong thực tế, máy chủ của bạn sẽ tan chảy dưới tải như vậy, nhưng hãy chơi nó ra.
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
Vì vậy, 50 triệu triệu lượt đoán mỗi giây. Nhanh thật! Đúng?
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
Vậy 6,8 giây ...
Hãy cố gắng đưa số đó xuống mức thân thiện hơn.
215,626,585,489,599 years
Hoặc thậm chí tốt hơn:
47917 times the age of the universe
Phải, đó là 47917 lần tuổi của vũ trụ ...
Về cơ bản, nó sẽ không bị nứt.
Vì vậy, để tổng hợp:
Cách tiếp cận tốt hơn mà tôi khuyên bạn nên lưu trữ cookie với ba phần.
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);
}
Sau đó, để xác nhận:
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);
}
}
}
Lưu ý: Không sử dụng mã thông báo hoặc kết hợp người dùng và mã thông báo để tra cứu bản ghi trong cơ sở dữ liệu của bạn. Luôn đảm bảo tìm nạp bản ghi dựa trên người dùng và sử dụng chức năng so sánh an toàn theo thời gian để so sánh mã thông báo được tìm nạp sau đó. Tìm hiểu thêm về các cuộc tấn công thời gian .
Bây giờ, điều rất quan trọng SECRET_KEY
là bí mật mật mã (được tạo ra bởi một cái gì đó giống như /dev/urandom
và / hoặc có nguồn gốc từ một đầu vào entropy cao). Ngoài ra, GenerateRandomToken()
cần phải được một nguồn ngẫu nhiên mạnh ( mt_rand()
không phải là đủ gần mạnh. Sử dụng một thư viện, chẳng hạn như RandomLib hoặc random_compat , hoặc mcrypt_create_iv()
với DEV_URANDOM
) ...
Các hash_equals()
là để ngăn chặn các cuộc tấn công thời gian . Nếu bạn sử dụng phiên bản PHP bên dưới PHP 5.6 thì chức năng hash_equals()
không được hỗ trợ. Trong trường hợp này, bạn có thể thay thế hash_equals()
bằng chức năng timeSafeCompare:
/**
* 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;
}