ตกลงให้ฉันทำแบบนี้โผงผาง: ถ้าคุณใส่ข้อมูลผู้ใช้หรือสิ่งที่ได้รับจากข้อมูลผู้ใช้ลงในคุกกี้เพื่อจุดประสงค์นี้คุณกำลังทำอะไรผิด
ที่นั่น ฉันเป็นคนพูดมันเอง. ตอนนี้เราสามารถไปยังคำตอบจริงได้
คุณถามอะไรผิดปกติกับการแฮชข้อมูล? มันลงมาที่ผิวสัมผัสและความปลอดภัยผ่านความสับสน
ลองนึกภาพหนึ่งวินาทีว่าคุณเป็นผู้โจมตี คุณเห็นคุกกี้การเข้ารหัสที่ตั้งค่าไว้สำหรับจดจำฉันในเซสชันของคุณ กว้าง 32 ตัวอักษร Gee นั่นอาจเป็น MD5 ...
ลองจินตนาการถึงวินาทีที่พวกเขารู้อัลกอริทึมที่คุณใช้ ตัวอย่างเช่น:
md5(salt+username+ip+salt)
ตอนนี้ผู้โจมตีทุกคนต้องทำคือกำลังดุร้าย "เกลือ" (ซึ่งไม่ใช่เกลือจริง ๆ แต่มากกว่านั้นในภายหลัง) และตอนนี้เขาสามารถสร้างโทเค็นปลอมทั้งหมดที่เขาต้องการด้วยชื่อผู้ใช้สำหรับที่อยู่ IP ของเขา! แต่การบังคับให้ใส่เกลือเป็นเรื่องยากใช่มั้ย อย่างแน่นอน แต่ GPU ในยุคปัจจุบันนั้นยอดเยี่ยมมาก และถ้าคุณใช้การสุ่มในที่มากพอ (ทำให้มันใหญ่พอ) มันจะล้มลงอย่างรวดเร็วและด้วยกุญแจของปราสาทของคุณ
ในระยะสั้นสิ่งเดียวที่ปกป้องคุณคือเกลือซึ่งไม่ได้ปกป้องคุณเท่าที่คุณคิด
แต่เดี๋ยวก่อน!
ทั้งหมดนี้เป็นการแจ้งล่วงหน้าว่าผู้โจมตีรู้อัลกอริทึม! ถ้ามันเป็นความลับและทำให้สับสนคุณก็ปลอดภัยใช่ไหม? ผิด แนวความคิดนั้นมีชื่อ: ความปลอดภัยผ่านความสับสนซึ่งไม่ควรเชื่อถือ
วิธีที่ดีกว่า
วิธีที่ดีกว่าคือการไม่ปล่อยให้ข้อมูลของผู้ใช้ออกจากเซิร์ฟเวอร์ยกเว้นรหัส
เมื่อผู้ใช้ล็อกอินสร้างโทเค็นแบบสุ่มขนาดใหญ่ (128 ถึง 256 บิต) เพิ่มลงในตารางฐานข้อมูลซึ่งจับคู่โทเค็นกับหมายเลขผู้ใช้แล้วส่งไปยังไคลเอนต์ในคุกกี้
เกิดอะไรขึ้นถ้าผู้โจมตีเดาโทเค็นแบบสุ่มของผู้ใช้รายอื่น
ลองทำคณิตศาสตร์กันที่นี่ เรากำลังสร้างโทเค็นแบบสุ่ม 128 บิต นั่นหมายความว่ามี:
possibilities = 2^128
possibilities = 3.4 * 10^38
ทีนี้เพื่อให้เห็นว่าตัวเลขนั้นมีจำนวนมากแค่ไหนลองจินตนาการว่าทุกเซิร์ฟเวอร์บนอินเทอร์เน็ต (สมมติว่าวันนี้มี 50,000,000 วัน) พยายามที่จะบังคับตัวเลขนั้นในอัตรา 1,000,000,000 ต่อวินาที ในความเป็นจริงเซิร์ฟเวอร์ของคุณจะละลายภายใต้ภาระดังกล่าว แต่ลองมาดูกัน
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
ดังนั้น 50 ล้านล้านเดาต่อวินาที เร็วมาก! ขวา?
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 พันล้านวินาที ...
มาลองดูตัวเลขที่เป็นมิตรกันมากขึ้น
215,626,585,489,599 years
หรือดีกว่า:
47917 times the age of the universe
ใช่นั่นคือ 47917 คูณอายุของจักรวาล ...
โดยทั่วไปมันจะไม่แตก
ดังนั้นเพื่อสรุป:
วิธีที่ดีกว่าที่ฉันแนะนำคือการจัดเก็บคุกกี้ด้วยสามส่วน
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);
}
จากนั้นในการตรวจสอบ:
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);
}
}
}
หมายเหตุ: อย่าใช้โทเค็นหรือชุดของผู้ใช้และโทเค็นเพื่อค้นหาบันทึกในฐานข้อมูลของคุณ ตรวจสอบให้แน่ใจเสมอว่าดึงข้อมูลตามผู้ใช้และใช้ฟังก์ชันการเปรียบเทียบที่ปลอดภัยต่อเวลาเพื่อเปรียบเทียบโทเค็นที่ดึงข้อมูลในภายหลัง เพิ่มเติมเกี่ยวกับการโจมตีระยะเวลา
ตอนนี้มันสำคัญมากที่SECRET_KEY
จะเป็นความลับเข้ารหัส (สร้างโดยสิ่งที่ชอบ/dev/urandom
และ / หรือได้มาจากการป้อนข้อมูลสูงเอนโทรปี) นอกจากนี้GenerateRandomToken()
จำเป็นต้องเป็นแหล่งที่มาแบบสุ่มที่แข็งแกร่ง ( mt_rand()
ไม่แรงพอที่จะใช้ไลบรารีเช่นRandomLibหรือrandom_compatหรือmcrypt_create_iv()
ด้วยDEV_URANDOM
) ...
hash_equals()
คือการป้องกันการโจมตีระยะเวลา หากคุณใช้เวอร์ชัน PHP ด้านล่าง PHP 5.6 ฟังก์ชั่นhash_equals()
นี้จะไม่รองรับ ในกรณีนี้คุณสามารถแทนที่hash_equals()
ด้วยฟังก์ชั่น 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;
}