자, 이것을 무뚝뚝하게하겠습니다 : 만약 사용자 데이터 또는이 목적을 위해 사용자 데이터에서 파생 된 것을 쿠키에 넣는다면, 당신은 뭔가 잘못하고있는 것입니다.
그곳에. 내가 말했어. 이제 실제 답변으로 넘어갈 수 있습니다.
사용자 데이터 해싱의 문제점은 무엇입니까? 글쎄, 그것은 모호함을 통해 노출 표면과 보안으로 귀착됩니다.
당신이 공격 자라고 잠시 상상해보십시오. 세션에서 remember-me에 대한 암호화 쿠키 세트가 표시됩니다. 32 자입니다. 사람. MD5 일 수도 있습니다.
또한 사용자가 사용한 알고리즘을 알고 있다고 잠시 상상해 봅시다. 예를 들면 다음과 같습니다.
md5(salt+username+ip+salt)
이제 공격자가해야 할 일은 "소금"(실제로 소금은 아니지만 나중에 더 자세히 설명)을 무차별 처리하는 것입니다. 이제 IP 주소의 모든 사용자 이름으로 원하는 모든 가짜 토큰을 생성 할 수 있습니다! 그러나 소금을 강제로 강제하는 것은 어렵습니다. 물론. 그러나 현대의 GPU는 그것을 능가합니다. 그리고 당신이 그것에 충분한 임의성을 사용하지 않으면 (충분히 크게 만드십시오), 그것은 빨리 떨어지고 성의 열쇠와 함께 떨어질 것입니다.
요컨대, 당신을 보호하는 유일한 것은 소금입니다. 생각만큼 많이 보호하지는 않습니다.
하지만 기다려!
이 모든 것이 공격자가 알고리즘을 알고 있다고 가정했습니다! 비밀스럽고 혼란 스러우면 안전합니까? 잘못되었습니다 . 그 사고 방식은 ' 불안을 통한 보안' 이라는 이름을 가지 는데, 절대로 의존 해서는 안됩니다 .
더 나은 방법
더 좋은 방법은 ID를 제외하고 사용자 정보가 서버를 떠나지 않도록하는 것입니다.
사용자가 로그인하면 큰 (128-256 비트) 임의 토큰을 생성하십시오. 토큰을 사용자 ID에 맵핑하는 데이터베이스 테이블에 추가 한 후 쿠키의 클라이언트로 전송하십시오.
침입자가 다른 사용자의 임의 토큰을 추측하면 어떻게됩니까?
자, 여기서 수학을하자. 128 비트 랜덤 토큰을 생성 중입니다. 즉, 다음이 있음을 의미합니다.
possibilities = 2^128
possibilities = 3.4 * 10^38
이제 그 숫자가 얼마나 터무니 없는지 보여주기 위해 인터넷상의 모든 서버 (오늘 5 만, 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 sextillion 초 ...
좀 더 친근한 숫자로 가져 오도록합시다.
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 5.6 이하의 PHP 버전을 사용하는 경우 기능 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;
}