IT이야기

PHP에서 사용자 로그인 시도를 제한하는 방법

cyworld 2021. 4. 3. 10:52
반응형

PHP에서 사용자 로그인 시도를 제한하는 방법


방금이 게시물을 읽고있었습니다 . Rapid-Fire 로그인 시도 방지에 대한 양식 기반 웹 사이트 인증대한 확실한 가이드 입니다.

모범 사례 # 1 : 다음과 같이 실패한 시도 횟수에 따라 증가하는 짧은 시간 지연 :

1 회 실패 = 지연 없음
2 회 실패 = 2 초 지연
3 회 실패 = 4 초 지연
4 회 실패 = 8 초 지연
5 회 실패 = 16 초 지연

이 계획을 공격하는 DoS는 매우 비실용적이지만 다른 한편으로는 지연이 기하 급수적으로 증가하기 때문에 잠재적으로 파괴적입니다.

PHP에서 로그인 시스템에 대해 이와 같은 것을 어떻게 구현할 수 있는지 궁금합니다.


스로틀 링을 단일 IP 또는 사용자 이름으로 연결하여 단순히 DoS 공격을 방지 할 수는 없습니다. 지옥,이 방법을 사용하여 빠른 로그인 시도를 실제로 막을 수는 없습니다.

왜? 스로틀 링 시도를 우회하기 위해 공격이 여러 IP 및 사용자 계정에 걸쳐있을 수 있기 때문입니다.

이상적으로는 사이트 전체에서 실패한 모든 로그인 시도를 추적하고이를 타임 스탬프에 연결해야한다고 다른 곳에 게시 한 적이 있습니다.

CREATE TABLE failed_logins (
    id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(16) NOT NULL,
    ip_address INT(11) UNSIGNED NOT NULL,
    attempted DATETIME NOT NULL,
    INDEX `attempted_idx` (`attempted`)
) engine=InnoDB charset=UTF8;

ip_address 필드에 대한 빠른 참고 : INET_ATON () 및 INET_NTOA ()를 사용하여 각각 데이터를 저장하고 데이터를 검색 할 수 있습니다. 이는 본질적으로 ip 주소를 부호없는 정수로 /에서 변환하는 것과 같습니다.

# example of insertion
INSERT INTO failed_logins SET username = 'example', ip_address = INET_ATON('192.168.0.1'), attempted = CURRENT_TIMESTAMP;
# example of selection
SELECT id, username, INET_NTOA(ip_address) AS ip_address, attempted;

주어진 시간 (이 예에서는 15 분) 동안 실패한 전체 로그인 수를 기반으로 특정 지연 임계 값을 결정합니다 . 시간이 지남에 따라 사용자 수와 암호를 기억하고 입력 할 수있는 사용자 수에 따라 변경 되므로 failed_logins테이블 에서 가져온 통계 데이터를 기반으로해야합니다.


> 10 failed attempts = 1 second
> 20 failed attempts = 2 seconds
> 30 failed attempts = reCaptcha

로그인 시도가 실패 할 때마다 테이블을 쿼리하여 주어진 시간 (예 : 15 분) 동안 실패한 로그인 수를 찾습니다.


SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute);

주어진 기간 동안 시도 횟수가 제한을 초과하는 경우, 제한을 적용하거나 지정된 기간 동안 실패한 시도 횟수가 임계 값보다 적을 때까지 모든 사용자가 보안 문자 (예 : reCaptcha)를 사용하도록합니다.

// array of throttling
$throttle = array(10 => 1, 20 => 2, 30 => 'recaptcha');

// retrieve the latest failed login attempts
$sql = 'SELECT MAX(attempted) AS attempted FROM failed_logins';
$result = mysql_query($sql);
if (mysql_affected_rows($result) > 0) {
    $row = mysql_fetch_assoc($result);

    $latest_attempt = (int) date('U', strtotime($row['attempted']));

    // get the number of failed attempts
    $sql = 'SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute)';
    $result = mysql_query($sql);
    if (mysql_affected_rows($result) > 0) {
        // get the returned row
        $row = mysql_fetch_assoc($result);
        $failed_attempts = (int) $row['failed'];

        // assume the number of failed attempts was stored in $failed_attempts
        krsort($throttle);
        foreach ($throttle as $attempts => $delay) {
            if ($failed_attempts > $attempts) {
                // we need to throttle based on delay
                if (is_numeric($delay)) {
                    $remaining_delay = time() - $latest_attempt - $delay;
                    // output remaining delay
                    echo 'You must wait ' . $remaining_delay . ' seconds before your next login attempt';
                } else {
                    // code to display recaptcha on login form goes here
                }
                break;
            }
        }        
    }
}

특정 임계 값에서 reCaptcha를 사용하면 여러 전면의 공격이 중지되고 일반 사이트 사용자는 합법적 인 로그인 시도 실패에 대해 상당한 지연을 경험하지 않습니다.


세션 정보 저장, 쿠키 정보 저장 또는 IP 정보 저장의 세 가지 기본 접근 방식이 있습니다.

세션 정보를 사용하면 최종 사용자 (공격자)가 강제로 새 세션을 호출하고 전술을 우회 한 다음 지연없이 다시 로그인 할 수 있습니다. 세션은 구현하기가 매우 간단합니다. 사용자의 마지막으로 알려진 로그인 시간을 세션 변수에 저장하고 현재 시간과 일치시키고 지연이 충분히 긴지 확인합니다.

쿠키를 사용하는 경우 공격자는 단순히 쿠키를 거부 할 수 있습니다. 이것은 실제로 실행 가능한 것이 아닙니다.

IP 주소를 추적하는 경우 IP 주소에서 로그인 시도를 저장해야합니다. 가급적이면 데이터베이스에 저장해야합니다. 사용자가 로그온을 시도 할 때 기록 된 IP 목록을 업데이트하기 만하면됩니다. 이 테이블을 적절한 간격으로 제거하여 일정 시간 동안 활성화되지 않은 IP 주소를 덤프해야합니다. 함정 (항상 함정이 있음)은 일부 사용자가 IP 주소를 공유하게 될 수 있으며 경계 조건에서 지연이 사용자에게 실수로 영향을 미칠 수 있다는 것입니다. 실패한 로그인을 추적하고 실패한 로그인 만 추적하므로 너무 많은 고통을주지 않아야합니다.


로그인 프로세스는 로그인 성공 및 실패 모두 속도를 줄여야합니다. 로그인 시도 자체는 약 1 초보다 빠르지 않아야합니다. 그렇다면 무차별 대입은 성공이 실패보다 짧기 때문에 시도가 실패했음을 알기 위해 지연을 사용합니다. 그러면 초당 더 많은 조합을 평가할 수 있습니다.

머신 당 동시 로그인 시도 횟수는로드 밸런서에 의해 제한되어야합니다. 마지막으로 동일한 사용자 또는 암호가 두 번 이상의 사용자 / 암호 로그인 시도에 의해 재사용되는지 추적하기 만하면됩니다. 인간은 분당 약 200 단어보다 빠르게 입력 할 수 없습니다. 따라서 분당 200 단어보다 빠른 연속 또는 동시 로그인 시도는 일련의 컴퓨터에서 발생합니다. 따라서 고객이 아닌 블랙리스트에 안전하게 연결할 수 있습니다. 호스트 당 블랙리스트 시간은 약 1 초를 초과 할 필요가 없습니다. 이것은 인간에게 결코 불편을 끼치 지 않을 것이지만, 직렬이든 병렬이든 무차별 대입 시도로 혼란을 겪습니다.

40 억 개의 개별 IP 주소에서 병렬로 실행되는 초당 하나의 조합으로 2 * 10 ^ 19 개의 조합이 검색 공간으로 소모되는 데 158 년이 걸립니다. 40 억 명의 공격자에 대해 사용자 당 하루 동안 지속하려면 최소 9 자리의 완전한 임의의 영숫자 암호가 필요합니다. 최소 13 자리, 1.7 * 10 ^ 20 조합의 암호문으로 사용자를 교육하는 것이 좋습니다.

이 지연은 공격자가 사이트를 무차별 대입하는 대신 암호 해시 파일을 훔치도록 동기를 부여합니다. 승인되고 명명 된 해싱 기술을 사용합니다. 전체 인터넷 IP 인구를 1 초 동안 금지하면 사람이 이해하지 못하는 병렬 공격의 효과가 제한됩니다. 마지막으로, 시스템에서 금지 시스템에 대한 응답없이 1 초에 1000 회 이상의 로그온 실패를 허용하면 보안 계획에 더 큰 문제가 있습니다. 먼저 자동 응답을 수정하십시오.


session_start();
$_SESSION['hit'] += 1; // Only Increase on Failed Attempts
$delays = array(1=>0, 2=>2, 3=>4, 4=>8, 5=>16); // Array of # of Attempts => Secs

sleep($delays[$_SESSION['hit']]); // Sleep for that Duration.

또는 Cyro가 제안한대로 :

sleep(2 ^ (intval($_SESSION['hit']) - 1));

약간 거칠지 만 기본 구성 요소가 있습니다. 이 페이지를 새로 고치면 새로 고칠 때마다 지연 시간이 길어집니다.

IP로 실패한 시도 횟수를 확인하는 데이터베이스에 카운트를 유지할 수도 있습니다. IP를 기반으로 사용하고 데이터를 귀하 측에 보관함으로써 사용자가 쿠키를 삭제하여 지연을 막을 수 없습니다.

기본적으로 시작 코드는 다음과 같습니다.

$count = get_attempts(); // Get the Number of Attempts

sleep(2 ^ (intval($count) - 1));

function get_attempts()
{
    $result = mysql_query("SELECT FROM TABLE WHERE IP=\"".$_SERVER['REMOTE_ADDR']."\"");
    if(mysql_num_rows($result) > 0)
    {
        $array = mysql_fetch_assoc($array);
        return $array['Hits'];
    }
    else
    {
        return 0;
    }
}

실패 시도를 IP별로 데이터베이스에 저장합니다. (로그인 시스템이 있으므로이를 수행하는 방법을 잘 알고 있다고 가정합니다.)

분명히 세션은 유혹적인 방법이지만 실제로 헌신적 인 사람은 스로틀을 완전히 우회하기 위해 실패한 시도에 대해 세션 쿠키를 삭제할 수 있다는 것을 쉽게 알 수 있습니다.

로그인을 시도 할 때 최근 (예 : 최근 15 분) 로그인 시도 횟수와 최근 시도 시간을 가져옵니다.

$failed_attempts = 3; // for example
$latest_attempt = 1263874972; // again, for example
$delay_in_seconds = pow(2, $failed_attempts); // that's 2 to the $failed_attempts power
$remaining_delay = time() - $latest_attempt - $delay_in_seconds;
if($remaining_delay > 0) {
    echo "Wait $remaining_delay more seconds, silly!";
}

세션을 사용할 수 있습니다. 사용자가 로그인에 실패 할 때마다 시도 횟수를 저장하는 값을 늘립니다. 시도 횟수에서 필요한 지연을 계산하거나 사용자가 세션에서 다시 시도 할 수있는 실제 시간을 설정할 수도 있습니다.

보다 신뢰할 수있는 방법은 특정 ipaddress에 대한 데이터베이스에 시도 및 새 시도 시간을 저장하는 것입니다.


IMHO, DOS 공격에 대한 방어는 PHP 코드가 아닌 웹 서버 수준 (또는 네트워크 하드웨어에서)에서 더 잘 처리됩니다.


나는 일반적으로 로그인 기록과 로그인 시도 테이블을 생성합니다. 시도 테이블은 사용자 이름, 암호, IP 주소 등을 기록합니다. 테이블에 대해 쿼리하여 지연이 필요한지 확인합니다. 주어진 시간 (예 : 1 시간)에 20 회 이상의 시도를 완전히 차단하는 것이 좋습니다.


위에서 논의한 바와 같이 세션, 쿠키 및 IP 주소는 효과적이지 않으며 모두 공격자가 조작 할 수 있습니다.

무차별 대입 공격을 방지하려면 제공된 사용자 이름을 기준으로 시도 횟수를 지정하는 것이 유일한 해결책입니다.하지만 이렇게하면 공격자가 유효한 사용자의 로그인을 차단하여 사이트를 DOS 할 수 있습니다.

예 :

$valid=check_auth($_POST['USERNAME'],$_POST['PASSWD']);
$delay=get_delay($_POST['USERNAME'],$valid);

if (!$valid) {
   header("Location: login.php");
   exit;
}
...
function get_delay($username,$authenticated)
{
    $loginfile=SOME_BASE_DIR . md5($username);
    if (@filemtime($loginfile)<time()-8600) {
       // last login was never or over a day ago
       return 0;
    }
    $attempts=(integer)file_get_contents($loginfile);
    $delay=$attempts ? pow(2,$attempts) : 0;
    $next_value=$authenticated ? 0 : $attempts + 1;
    file_put_contents($loginfile, $next_value);
    sleep($delay); // NB this is done regardless if passwd valid
    // you might want to put in your own garbage collection here
 }

작성된대로이 절차는 보안 정보를 유출합니다. 즉, 시스템을 공격하는 누군가가 사용자가 로그인 할 때를 볼 수 있습니다 (공격자 시도에 대한 응답 시간이 0으로 떨어집니다). 파일의 이전 지연 및 타임 스탬프를 기반으로 지연이 계산되도록 알고리즘을 조정할 수도 있습니다.

HTH

씨.


물론이 경우 쿠키 나 세션 기반 방법은 쓸모가 없습니다. 애플리케이션은 이전 로그인 시도의 IP 주소 또는 타임 스탬프 (또는 둘 다)를 확인해야합니다.

공격자가 요청을 시작할 IP가 두 개 이상이면 IP 확인을 우회 할 수 있으며 여러 사용자가 동일한 IP에서 서버에 연결하는 경우 문제가 될 수 있습니다. 후자의 경우 누군가가 여러 번 로그인에 실패하면 동일한 IP를 공유하는 모든 사람이 특정 기간 동안 해당 사용자 이름으로 로그인하지 못하게됩니다.

타임 스탬프 검사에는 위와 동일한 문제가 있습니다. 누구나 여러 번 시도하는 것만으로 다른 모든 사람이 특정 계정에 로그인하지 못하도록 방지 할 수 있습니다. 마지막 시도를 오래 기다리지 않고 보안 문자를 사용하는 것이 좋은 해결 방법 일 수 있습니다.

로그인 시스템이 방지해야하는 유일한 추가 사항은 시도 확인 기능의 경쟁 조건입니다. 예를 들어, 다음 의사 코드에서

$time = get_latest_attempt_timestamp($username);
$attempts = get_latest_attempt_number($username);

if (is_valid_request($time, $attempts)) {
    do_login($username, $password);
} else {
    increment_attempt_number($username);
    display_error($attempts);
}

공격자 가 로그인 페이지에 동시 요청을 보내면 어떻게됩니까 ? 아마도 모든 요청은 동일한 우선 순위로 실행될 것이며 다른 요청이 두 번째 줄을 지나기 전에 increment_attempt_number 명령에 대한 요청이 없을 가능성이 있습니다. 따라서 모든 요청은 동일한 $ time 및 $ attempts 값을 가져와 실행됩니다. 이러한 종류의 보안 문제를 방지하는 것은 복잡한 응용 프로그램에서 어려울 수 있으며 데이터베이스의 일부 테이블 / 행을 잠그고 잠금을 해제하여 응용 프로그램 속도를 떨어 뜨립니다.


짧은 대답은 다음과 같습니다. 이렇게하지 마십시오. 무차별 대입으로부터 자신을 보호 할 수 없으며 상황을 더 악화시킬 수도 있습니다.

제안 된 솔루션 중 어느 것도 작동하지 않습니다. 스로틀 링을위한 매개 변수로 IP를 사용하는 경우 공격자는 엄청난 수의 IP에 걸쳐 공격을 스팬합니다. 세션 (쿠키)을 사용하면 공격자는 모든 쿠키를 삭제합니다. 당신이 생각할 수있는 모든 것의 요약은 무차별 대입 공격자가 극복 할 수없는 것은 전혀 없다는 것입니다.

하지만 한 가지가 있습니다. 로그인을 시도한 사용자 이름에만 의존합니다. 따라서 다른 모든 매개 변수를 살펴 보지 않고 사용자가 로그인 및 제한을 시도한 빈도를 추적합니다. 그러나 공격자는 당신을 해치고 싶어합니다. 그가 이것을 인식하면 그는 사용자 이름도 무차별 대입 할 것입니다.

이로 인해 거의 모든 사용자가 로그인을 시도 할 때 최대 값으로 제한됩니다. 귀하의 웹 사이트는 쓸모가 없게됩니다. 공격자 : 성공.

일반적으로 약 200ms 동안 비밀번호 확인을 지연시킬 수 있습니다. 웹 사이트 사용자는이를 거의 알아 차리지 못할 것입니다. 그러나 무자비한 힘이 될 것입니다. (다시 그는 여러 IP에 걸쳐있을 수 있습니다.) 그러나이 모든 것은 프로그래밍 방식으로 할 수 없기 때문에 무차별 대입이나 DDoS로부터 보호 할 수 없습니다.

이를 수행하는 유일한 방법은 인프라를 사용하는 것입니다.

MD5 또는 SHA-x 대신 bcrypt를 사용하여 암호를 해시해야합니다. 이렇게하면 누군가가 데이터베이스를 훔친 경우 암호 해독이 훨씬 더 어려워집니다 (공유 또는 관리 호스트에있는 것 같기 때문).

실망 시켜서 미안하지만 여기에있는 모든 솔루션에는 약점이 있으며 백엔드 로직 내에서이를 극복 할 방법이 없습니다.


cballuo는 훌륭한 답변을 제공했습니다. mysqli를 지원하는 업데이트 된 버전을 제공하여 호의에 보답하고 싶었습니다. 나는 SQL과 다른 작은 것들에서 테이블 / 필드 열을 약간 변경했지만 mysqli와 동등한 것을 찾는 모든 사람들에게 도움이 될 것입니다.

function get_multiple_rows($result) {
  $rows = array();
  while($row = $result->fetch_assoc()) {
    $rows[] = $row;
  }
  return $rows;
}

$throttle = array(10 => 1, 20 => 2, 30 => 5);

$query = "SELECT MAX(time) AS attempted FROM failed_logins";    

if ($result = $mysqli->query($query)) {

    $rows = get_multiple_rows($result);

$result->free();

$latest_attempt = (int) date('U', strtotime($rows[0]['attempted'])); 

$query = "SELECT COUNT(1) AS failed FROM failed_logins WHERE time > DATE_SUB(NOW(), 
INTERVAL 15 minute)";   

if ($result = $mysqli->query($query)) {

$rows = get_multiple_rows($result);

$result->free();

    $failed_attempts = (int) $rows[0]['failed'];

    krsort($throttle);
    foreach ($throttle as $attempts => $delay) {
        if ($failed_attempts > $attempts) {
                echo $failed_attempts;
                $remaining_delay = (time() - $latest_attempt) - $delay;

                if ($remaining_delay < 0) {
                echo 'You must wait ' . abs($remaining_delay) . ' seconds before your next login attempt';
                }                

            break;
        }
     }        
  }
}

참조 URL : https://stackoverflow.com/questions/2090910/how-can-i-throttle-user-login-attempts-in-php

반응형