Безопасен ли этот скрипт входа в PHP / MySQL? ОБНОВЛЕННЫЙ КОД - PullRequest
3 голосов
/ 02 января 2011

Привет,

Разработанный мною сайт был взломан сегодня, работая над контролем ущерба в данный момент.Доступ к двум учетным записям пользователей, включая основного администратора, осуществлялся без авторизации.Пожалуйста, ознакомьтесь с используемым сценарием входа в систему, и мы будем благодарны за любую информацию о дырах в безопасности.Я не уверен, что это была инъекция SQL или, возможно, нарушение на компьютере, который использовался для доступа к этой области в прошлом.

Спасибо

<?php
    //Start session
    session_start();
    //Include DB config
    require_once('config.php');

    //Error message array
    $errmsg_arr = array();
    $errflag = false;
    //Connect to mysql server
    $link = mysql_connect(DB_HOST, DB_USER, DB_PASSWORD);
    if(!$link) {
        die('Failed to connect to server: ' . mysql_error());
    }
    //Select database
    $db = mysql_select_db(DB_DATABASE);
    if(!$db) {
        die("Unable to select database");
    }

    //Function to sanitize values received from the form. Prevents SQL injection
    function clean($str) {
        $str = @trim($str);
        if(get_magic_quotes_gpc()) {
            $str = stripslashes($str);
        }
        return mysql_real_escape_string($str);
    }
    //Sanitize the POST values
    $login = clean($_POST['login']);
    $password = clean($_POST['password']);

    //Input Validations
    if($login == '') {
        $errmsg_arr[] = 'Login ID missing';
        $errflag = true;
    }
    if($password == '') {
        $errmsg_arr[] = 'Password missing';
        $errflag = true;
    }

    //If there are input validations, redirect back to the login form
    if($errflag) {
        $_SESSION['ERRMSG_ARR'] = $errmsg_arr;
        session_write_close();
        header("location: http://somewhere.com");
        exit();
    }

    //Create query
    $qry="SELECT * FROM user_control WHERE username='$login' AND password='".md5($_POST['password'])."'";
    $result=mysql_query($qry);

    //Check whether the query was successful or not
    if($result) {
        if(mysql_num_rows($result) == 1) {
            //Login Successful
            session_regenerate_id();
            //Collect details about user and assign session details
            $member = mysql_fetch_assoc($result);
            $_SESSION['SESS_MEMBER_ID'] = $member['user_id'];
            $_SESSION['SESS_USERNAME'] = $member['username'];
            $_SESSION['SESS_FIRST_NAME'] = $member['name_f'];
            $_SESSION['SESS_LAST_NAME'] = $member['name_l'];
            $_SESSION['SESS_STATUS'] = $member['status'];
            $_SESSION['SESS_LEVEL'] = $member['level'];
            //Get Last Login
            $_SESSION['SESS_LAST_LOGIN'] = $member['lastLogin'];
            //Set Last Login info
            $qry = "UPDATE user_control SET lastLogin = DATE_ADD(NOW(), INTERVAL 1 HOUR) WHERE user_id = $member[user_id]";
            $login = mysql_query($qry) or die(mysql_error());
            session_write_close();
            if ($member['level'] != "3" || $member['status'] == "Suspended") {
                header("location: http://somewhere.com");
            } else {
                header("location: http://somewhere.com");
            }
            exit();
        }else {
            //Login failed
            header("location: http://somewhere.com");
            exit();
        }
    }else {
        die("Query failed");
    }
?>

ОБНОВЛЕНИЕ

Вот обновленная версия скрипта безопасности, пожалуйста, дайте мне знать, что вы думаете.Добавлен небольшой SALT и таблица для блокировки как IP-адресов (отключение самой формы входа в систему), так и отдельных пользователей после неудачной аутентификации четыре раза.Электронное письмо за майское время также отправляется администратору, и пользователь получает уведомление о превышении лимита.

Любые критические замечания будут приветствоваться!

<?php
    //Start session
    session_start();
    //Include DB config
    include $_SERVER['DOCUMENT_ROOT'] . '/includes/pdo_conn.inc.php';

    //Error message array
    $errmsg_arr = array();
    $errflag = false;

    //Function to sanitize values received from the form. Prevents SQL injection
    function clean($str) {
        $str = @trim($str);
        if(get_magic_quotes_gpc()) {
            $str = stripslashes($str);
        }
        return $str;
    }

    //Define a SALT
    define('SALT', 'heylookitssuperman');

    //Sanitize the POST values
    $login = clean($_POST['login']);
    $password = clean($_POST['password']);
    //Encrypt password
    $encryptedPassword = md5(SALT . $password);
    //Input Validations
    //Obtain IP address and check for past failed attempts
    $ip_address = $_SERVER['REMOTE_ADDR'];
    $checkIPBan = $db->prepare("SELECT COUNT(*) FROM ip_ban WHERE ipAddr = ? OR login = ?");
    $checkIPBan->execute(array($ip_address, $login));
    $numAttempts = $checkIPBan->fetchColumn();
    //If there are 4 failed attempts, send back to login and temporarily ban IP address
    if ($numAttempts == 1) {
        $getTotalAttempts = $db->prepare("SELECT attempts FROM ip_ban WHERE ipAddr = ? OR login = ?");
        $getTotalAttempts->execute(array($ip_address, $login));
        $totalAttempts = $getTotalAttempts->fetch();
        $totalAttempts = $totalAttempts['attempts'];
        if ($totalAttempts >= 4) {
            //Send Mayday SMS
            $to = "admin@somewhere.com";
            $subject = "Banned Account - $login";
            $mailheaders = 'From: noreply@somewhere.com' . "\r\n";
            $mailheaders .= 'Reply-To: noreply@somewhere.com' . "\r\n";
            $mailheaders .= 'MIME-Version: 1.0' . "\r\n";
            $mailheaders .= 'Content-type: text/html; charset=iso-8859-1' . "\r\n";
            $msg = "<p>IP Address - " . $ip_address . ", Username - " . $login . "</p>";
            mail($to, $subject, $msg, $mailheaders);
            $setAccountBan = $db->query("UPDATE ip_ban SET isBanned = 1 WHERE ipAddr = '$ip_address'");
            $setAccountBan->execute();
            $errmsg_arr[] = 'Too Many Login Attempts';
            $errflag = true;    
        }
    }
    if($login == '') {
        $errmsg_arr[] = 'Login ID missing';
        $errflag = true;
    }
    if($password == '') {
        $errmsg_arr[] = 'Password missing';
        $errflag = true;
    }

    //If there are input validations, redirect back to the login form
    if($errflag) {
        $_SESSION['ERRMSG_ARR'] = $errmsg_arr;
        session_write_close();
        header('Location: http://somewhere.com/login.php');
        exit();
    }

    //Query database
    $loginSQL = $db->prepare("SELECT password FROM user_control WHERE username = ?");
    $loginSQL->execute(array($login));
    $loginResult = $loginSQL->fetch();

    //Compare passwords
    if($loginResult['password'] == $encryptedPassword) {
        //Login Successful
        session_regenerate_id();
        //Collect details about user and assign session details
        $getMemDetails = $db->prepare("SELECT * FROM user_control WHERE username = ?");
        $getMemDetails->execute(array($login));
        $member = $getMemDetails->fetch();
        $_SESSION['SESS_MEMBER_ID'] = $member['user_id'];
        $_SESSION['SESS_USERNAME'] = $member['username'];
        $_SESSION['SESS_FIRST_NAME'] = $member['name_f'];
        $_SESSION['SESS_LAST_NAME'] = $member['name_l'];
        $_SESSION['SESS_STATUS'] = $member['status'];
        $_SESSION['SESS_LEVEL'] = $member['level'];
        //Get Last Login
        $_SESSION['SESS_LAST_LOGIN'] = $member['lastLogin'];
        //Set Last Login info
        $updateLog = $db->prepare("UPDATE user_control SET lastLogin = DATE_ADD(NOW(), INTERVAL 1 HOUR), ip_addr = ? WHERE user_id = ?");
        $updateLog->execute(array($ip_address, $member['user_id']));
        session_write_close();
        //If there are past failed log-in attempts, delete old entries
        if ($numAttempts > 0) {
            //Past failed log-ins from this IP address. Delete old entries
            $deleteIPBan = $db->prepare("DELETE FROM ip_ban WHERE ipAddr = ?");
            $deleteIPBan->execute(array($ip_address));
        }
        if ($member['level'] != "3" || $member['status'] == "Suspended") {
            header("location: http://somewhere.com");
        } else {
            header('Location: http://somewhere.com');
        }
        exit();
    } else {
        //Login failed. Add IP address and other details to ban table
        if ($numAttempts < 1) {
        //Add a new entry to IP Ban table
        $addBanEntry = $db->prepare("INSERT INTO ip_ban (ipAddr, login, attempts) VALUES (?,?,?)");
        $addBanEntry->execute(array($ip_address, $login, 1));
        } else {
            //increment Attempts count 
            $updateBanEntry = $db->prepare("UPDATE ip_ban SET ipAddr = ?, login = ?, attempts = attempts+1 WHERE ipAddr = ? OR login = ?");
            $updateBanEntry->execute(array($ip_address, $login, $ip_address, $login));
        }
        header('Location: http://somewhere.com/login.php');
        exit();
    }
?>

Ответы [ 3 ]

3 голосов
/ 02 января 2011

Это ни в коем случае не является всеобъемлющим и может включать в себя элементы общего обзора:

  • Вызов die() при сбое соединения вызывает mysql_error(), что может привести к утечке конфиденциальной информации (база данныхhost, username)
  • В общем, всегда пытайтесь передать второй аргумент (дескриптор соединения) в mysql_real_escape_string(), что позволяет ему убедиться, что значение правильно экранировано в отношении набора символов и др.соединения
  • Ваш header() вызов содержит строку "location";правильное имя заголовка - «Местоположение»
  • Вы очищаете $_POST['password'], но затем внедряете его обратно в SQL с помощью md5( $_POST['password'] );хотя (из-за поведения md5()) это не создаст уязвимости, оно потенциально может нарушиться в среде, в которой было включено get_magic_quotes_gpc(), и является немного непоследовательным
  • Ваш вызов mysql_query() не определяет дескриптор соединения - хотя в этом сценарии вряд ли найдется еще один, я хотел бы быть явным при разговоре с MySQL - он дает мне теплые размышления

Youпохоже, что хэши паролей хранились - хорошо - хотя и с использованием MD5 (не очень хорошо, возможно, но), но без всяких засолок - это означает, что если кто-то завладеет ваши хэши паролей, то они могут использовать радужные таблицы / грубую силу дляпопробуйте взломать пароли.Тем не менее, вы можете утверждать (и я бы с радостью согласился), что если кто-то попадет на коробку и получит эти данные, у вас возникнут другие потенциальные проблемы.

Я бы порекомендовал прочитать http://chargen.matasano.com/chargen/2007/9/7/enough-with-the-rainbow-tables-what-you-need-to-know-about-s.html,, который объяснит, почему соль для каждого пользователя поможет, и реальная причина, по которой MD5, вероятно, больше не является лучшим выбором для функции хеширования пароля.

Не желая выходить за рамки этого ответаи спекулировать слишком много;может ли угонщик сеанса быть здесь виновником?(Я ничего не знаю о том, как возобновляются ваши сеансы, но кажется разумным, что ваш код может «доверять» данным сеанса. Конечно, это непросто победить наверняка - привязка сеансов к IP-адресам - хорошее началоНапример.)

1 голос
/ 02 января 2011

Вы можете добавить соль к сохраненным паролям, чтобы предотвратить атаки по словарю и rainbowtables.

Вы также можете хранить пароли в более надежных хэш-кодах / хэш-кодах шифрования вместо MD5.

Проверьте, например: http://www.codinghorror.com/blog/2007/09/youre-probably-storing-passwords-incorrectly.html

Вы также можете временно приостановить IP-адреса, если слишком много попыток входа не удалось. Ибо скажем, час.

Таким образом, вы можете предотвратить атаки грубой силы. Ну, на самом деле это не помешает, это, по крайней мере, делает это намного сложнее.

0 голосов
/ 02 января 2011

Я не эксперт по SQL, но я знаю, что, как правило, не рекомендуется хранить пароли (или хэши паролей) в базе данных.Как правило, гораздо безопаснее зашифровать файл с помощью пароля, затем принять пароль через форму и попытаться расшифровать его, используя хэш пароля.Сюда;никакая информация о самом пароле никогда не сохраняется на жестком диске сервера, и никто не может расшифровать файл (или запись в базе данных) без пароля, даже если у них был физический доступ к серверу или доступ к файлу базы данных.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...