비밀번호 분실에 대한 식별자를 생성하고 싶습니다. mt_rand ()와 함께 타임 스탬프를 사용하여 할 수 있다고 읽었지만 일부 사람들은 타임 스탬프가 매번 고유하지 않을 수 있다고 말합니다. 그래서 나는 여기서 약간 혼란 스럽습니다. 타임 스탬프를 사용하여 할 수 있습니까?
질문
사용자 정의 길이의 무작위 / 고유 토큰을 생성하는 가장 좋은 방법은 무엇입니까?
여기에 많은 질문이 있다는 것을 알고 있지만 다른 사람들의 다른 의견을 읽은 후 더 혼란스러워집니다.
답변
PHP에서는 random_bytes()
. 이유 : 암호 알림 토큰을 얻을 수있는 방법을 찾고 있으며, 일회성 로그인 자격 증명 인 경우 실제로 보호 할 데이터 (즉, 전체 사용자 계정)가 있습니다.
따라서 코드는 다음과 같습니다.
//$length = 78 etc
$token = bin2hex(random_bytes($length));
업데이트 : 이 답변의 이전 버전 은 uniqid()
고유성뿐만 아니라 보안 문제가 있으면 잘못된 것입니다. uniqid()
본질적 microtime()
으로 일부 인코딩 만 사용합니다. microtime()
서버 에서 에 대한 정확한 예측을 얻는 간단한 방법이 있습니다 . 공격자는 암호 재설정 요청을 발행 한 다음 몇 가지 가능한 토큰을 시도 할 수 있습니다. 추가 엔트로피가 비슷하게 약하기 때문에 more_entropy를 사용하는 경우에도 가능합니다. 이 점을 지적 해 주신 @NikiC 와 @ScottArciszewski 에게 감사드립니다 .
자세한 내용은
답변
이것은 ‘최상의 임의’요청에 응답합니다.
Security.StackExchange 의 Adi의 답변 1 에는 이에 대한 솔루션이 있습니다.
OpenSSL을 지원하는지 확인하세요.이 한 줄짜리로 잘못되지 않을 것입니다.
$token = bin2hex(openssl_random_pseudo_bytes(16));
1. Adi, Mon Nov 12 2018, Celeritas, “Generating an unguessable token for confirm e-mails”, Sep 20 ’13 at 7:06, https://security.stackexchange.com/a/40314/
답변
허용 된 답변 ( md5(uniqid(mt_rand(), true))
) 의 이전 버전 은 안전하지 않으며 가능한 출력은 약 2 ^ 60 개뿐입니다. 저예산 공격자에 대한 약 1 주일 내에 무차별 대입 검색 범위 내에 있습니다.
mt_rand()
예측 가능 (엔트로피 31 비트까지만 추가)uniqid()
최대 29 비트의 엔트로피 만 더합니다.md5()
엔트로피를 추가하지 않고 결정 론적으로 혼합합니다.
때문에 56 비트 DES 키가 될 수있는 약 24 시간 동안 무차별 강제 하고 평균 케이스 엔트로피 (59)에 대한 비트를 가질 것이며, 우리 팔일 약 2 ^ 2분의 59 ^ 56 = 계산할 수있다. 이 토큰 확인이 구현되는 방법에 따라 실제로 타이밍 정보를 유출하고 유효한 재설정 토큰의 처음 N 바이트를 추론 할 수 있습니다.
질문은 “모범 사례”에 관한 것이며 다음으로 시작됩니다.
비밀번호 분실에 대한 식별자를 생성하고 싶습니다
…이 토큰에 암시 적 보안 요구 사항이 있다고 추론 할 수 있습니다. 그리고 난수 생성기에 보안 요구 사항을 추가 할 때 가장 좋은 방법은 항상 암호화 된 보안 의사 난수 생성기 (약칭 CSPRNG)를 사용하는 것입니다.
CSPRNG 사용
PHP 7에서는 사용할 수 있습니다 bin2hex(random_bytes($n))
(여기서는 $n
15보다 큰 정수).
PHP 5에서는을 사용 random_compat
하여 동일한 API를 노출 할 수 있습니다 .
또한, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
당신이 경우 ext/mcrypt
설치. 또 다른 좋은 한 줄은 bin2hex(openssl_random_pseudo_bytes($n))
.
유효성 검사기에서 조회 분리
PHP의 보안 “remember me”쿠키에 대한 이전 작업에서 가져온 , 앞서 언급 한 타이밍 누출 (일반적으로 데이터베이스 쿼리에 의해 도입 됨)을 완화하는 유일한 효과적인 방법은 조회와 유효성 검사를 분리하는 것입니다.
테이블이 다음과 같은 경우 (MySQL) …
CREATE TABLE account_recovery (
id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT
userid INTEGER(11) UNSIGNED NOT NULL,
token CHAR(64),
expires DATETIME,
PRIMARY KEY(id)
);
… 다음 selector
과 같이 열을 하나 더 추가해야합니다 .
CREATE TABLE account_recovery (
id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT
userid INTEGER(11) UNSIGNED NOT NULL,
selector CHAR(16),
token CHAR(64),
expires DATETIME,
PRIMARY KEY(id),
KEY(selector)
);
CSPRNG 사용 암호 재설정 토큰이 발급되면 두 값을 모두 사용자에게 보내고 선택기와 임의 토큰의 SHA-256 해시를 데이터베이스에 저장합니다. 선택기를 사용하여 해시 및 사용자 ID를 가져오고 .NET을 사용하여 데이터베이스에 저장된 토큰과 함께 사용자가 제공하는 토큰의 SHA-256 해시를 계산합니다 hash_equals()
.
예제 코드
PDO를 사용하여 PHP 7 (또는 random_compat을 사용하는 5.6)에서 재설정 토큰 생성 :
$selector = bin2hex(random_bytes(8));
$token = random_bytes(32);
$urlToEmail = 'http://example.com/reset.php?'.http_build_query([
'selector' => $selector,
'validator' => bin2hex($token)
]);
$expires = new DateTime('NOW');
$expires->add(new DateInterval('PT01H')); // 1 hour
$stmt = $pdo->prepare("INSERT INTO account_recovery (userid, selector, token, expires) VALUES (:userid, :selector, :token, :expires);");
$stmt->execute([
'userid' => $userId, // define this elsewhere!
'selector' => $selector,
'token' => hash('sha256', $token),
'expires' => $expires->format('Y-m-d\TH:i:s')
]);
사용자 제공 재설정 토큰 확인 :
$stmt = $pdo->prepare("SELECT * FROM account_recovery WHERE selector = ? AND expires >= NOW()");
$stmt->execute([$selector]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($results)) {
$calc = hash('sha256', hex2bin($validator));
if (hash_equals($calc, $results[0]['token'])) {
// The reset token is valid. Authenticate the user.
}
// Remove the token from the DB regardless of success or failure.
}
이러한 코드 스 니펫은 완전한 솔루션은 아니지만 (입력 유효성 검사 및 프레임 워크 통합을 피했습니다) 수행 할 작업의 예가되어야합니다.
답변
DEV_RANDOM을 사용할 수도 있습니다. 여기서 128 = 생성 된 토큰 길이의 1/2입니다. 아래 코드는 256 개의 토큰을 생성합니다.
$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
답변
이것은 매우 임의의 토큰이 필요할 때 유용 할 수 있습니다.
<?php
echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>