这个PHP/MySQL登录脚本是否安全?更新的代码

3

你好,

今天我的一个网站被攻击了,目前正在进行损害控制。两个用户账户,包括主要管理员的账户,未经授权就被访问了。请查看使用的登录脚本,如果有安全漏洞的任何信息将不胜感激。我不确定这是否是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();
    }
?>

有一个非常简单的解决SQL注入的方法:存储过程。使用存储过程,攻击者可以使用各种字符串,但它只会执行您预期的相同语句。当然,这并不能保护业务错误,比如暴露的cookie、会话劫持等等。但它避免了手动构建SQL语句所带来的所有问题。 - datenwolf
1
你问题的正确答案是:永远不可能。无论你创造什么,它都不可能是安全的。尽你所能避免成为“低垂的果实”,并密切关注所有数据访问点。至于脚本方面,我强烈建议尽可能学习/使用“预处理语句”(又称“存储过程”),因为它不仅可以防止SQL注入,还可以提高数据访问速度(数据库缓存等)。 - zzzzBov
1
我想补充一下:对于安全问题积极主动的做法也值得点赞。作为开发人员,愿意识别自己不知道的东西并寻求帮助,这显示了一定的成熟度。 - zzzzBov
3个回答

3
这并不全面,可能包含一般审查元素:
  • 在连接失败时,die() 调用 mysql_error(),可能泄露敏感信息(数据库主机、用户名)
  • 通常情况下,始终尝试将第二个参数(连接句柄)传递给 mysql_real_escape_string(),它可以确保该值在连接的字符集等方面得到正确转义
  • 您的 header() 调用包含字符串 "location";正确的标题名称是 "Location"
  • 您对 $_POST['password'] 进行了消毒,但随后使用 md5( $_POST['password'] ) 将其注入回 SQL 中;虽然(由于 md5() 的行为),这不会引入漏洞,但在打开了 get_magic_quotes_gpc() 的环境中,它有可能出现问题,并且有点不一致
  • 您对 mysql_query() 的调用没有指定连接句柄——虽然在此脚本中,不太可能有其他连接句柄存在,但当我与 MySQL 交互时,我喜欢明确表示,这让我感到温暖和舒适

您似乎正在存储密码哈希值——不错——尽管使用 MD5(可能不太好),但没有任何形式的盐——这意味着如果有人拿到了您的密码哈希值,那么他们可以使用彩虹表/暴力破解来尝试破解密码。也就是说,您可能会争辩(我很乐意接受)如果有人进入了系统并获得了这些数据,那么您可能还有其他潜在问题。

我建议阅读 http://chargen.matasano.com/chargen/2007/9/7/enough-with-the-rainbow-tables-what-you-need-to-know-about-s.html,其中将解释为什么每个用户的盐将有所帮助,以及为什么 MD5 可能不再是最佳的密码哈希函数选择。

不想超出此答案的范围,并过多地推测;会话劫持是否是罪魁祸首?(我不知道您的会话如何恢复,但您的代码似乎“信任”会话数据。当然,这并不容易确定——例如,将会话绑定到 IP 地址是一个不错的开始。)


非常感谢您的智慧之言。看起来是时候升级那段代码了,我很后悔没有及时跟进。但我想这就是我们作为开发人员学习的方式,对吧?至于会话劫持,在这种情况下不太可能发生,但您提出的IP地址建议很好,我也会实施。再次感谢! - NightMICU

1

您可以在存储密码时添加盐,以防止字典和彩虹表攻击。

您还可以将密码存储在更强的哈希/加密哈希中,而不是MD5。

例如,请查看:http://www.codinghorror.com/blog/2007/09/youre-probably-storing-passwords-incorrectly.html

如果登录尝试失败次数过多,您还可以暂时挂起IP地址。比如一个小时。

这样,您就可以防止暴力破解攻击。虽然不能完全防止,但至少会使它变得更加困难。


-3

我不是SQL专家,但我知道通常在数据库中存储密码(或密码哈希)不是一个好的安全实践。通常更安全的做法是使用密码加密文件,然后通过表单接受密码并尝试使用密码哈希解密它。这样,关于密码本身的任何信息都不会存储在服务器硬盘上,即使有人可以物理访问服务器或访问数据库文件,也无法解密文件(或数据库条目)。


这是针对拥有多个用户的网站。密码以MD5加密形式存储,并在登录时进行加密,比较两个MD5加密密码。 - NightMICU
你不应该对用户密码进行加密,而是应该进行加盐和哈希处理。哈希无法被反向解密,适当的盐可以使强密码免受暴力攻击或彩虹表攻击的影响。 - Erik
通常情况下,暴力攻击不会因添加盐而受到影响。因为它只是随机尝试凭据,直到找到正确的凭据为止。 - PeeHaa
似乎该网站包含用户登录凭据的MySQL数据库表并未受到攻击,该个人只是登录了该网站的CMS。这让我想到了SQL注入的可能性,尽管使用的脚本应该已经防止了这种情况。第二个被攻击的帐户属于从不使用该系统的人,这很奇怪。 - NightMICU
1
我认为Erik指的是对密码哈希本身进行暴力攻击,这在加盐(“nonce”)哈希中变得指数级更难,并且这也是普通彩虹表变得无用的原因。 - Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接