请批评这个 PHP 登录脚本

发布于 2024-10-10 13:37:40 字数 6582 浏览 0 评论 0原文

我开发的一个网站最近遭到破坏,很可能是受到暴力攻击或彩虹表攻击。原始登录脚本没有SALT,密码以MD5存储。

以下是更新的脚本,其中包含 SALT 和 IP 地址禁止。此外,如果同一 IP 地址或帐户尝试 4 次登录失败,它将发送 Mayday 电子邮件和短信并禁用该帐户。请仔细检查一下,让我知道哪些地方可以改进,哪些地方缺失,哪些地方很奇怪。

<?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, the one here is for demo
    define('SALT', '63Yf5QNA');

    //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 = "[email protected]";
            $subject = "Banned Account - $login";
            $mailheaders = 'From: [email protected]' . "\r\n";
            $mailheaders .= 'Reply-To: [email protected]' . "\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();
    }
?>

编辑

好的,这是我随机盐的尝试。首先,创建要插入表中的盐:

define('SALT_LENGTH', 15);
function createSalt()
{
    $key = '!@#$%^&*()_+=-{}][;";/?<>.,';
    $salt = substr(hash('sha512',uniqid(rand(), true).$key.microtime()), 0, SALT_LENGTH);
    return $salt;
}
$salt = createSalt()
//More prep for entering into table...

然后使用随机盐生成哈希:

$hash = hash('sha256', $salt . $pw); //$pw is the cleaned user submitted password

当用户登录时,使用存储的随机生成的盐比较存储的哈希:

$loginHash = hash('sha256', $dbSalt . $pw);
if ($loginHash == $dbHash) {
    //Logged in
} else {
    //Failed
}

看起来怎么样?

A site I developed was recently compromised, most likely by a brute force or Rainbow Table attack. The original log-in script did not have a SALT, passwords were stored in MD5.

Below is an updated script, complete with SALT and IP address banning. In addition, it will send a Mayday email and SMS and disable the account should the same IP address or account attempt 4 failed log-ins. Please look it over and let me know what could be improved, what is missing, and what is just plain strange.

<?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, the one here is for demo
    define('SALT', '63Yf5QNA');

    //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 = "[email protected]";
            $subject = "Banned Account - $login";
            $mailheaders = 'From: [email protected]' . "\r\n";
            $mailheaders .= 'Reply-To: [email protected]' . "\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();
    }
?>

EDIT

Okay, here is my attempt at a random Salt. First, create the salt to be inserted into the table:

define('SALT_LENGTH', 15);
function createSalt()
{
    $key = '!@#$%^&*()_+=-{}][;";/?<>.,';
    $salt = substr(hash('sha512',uniqid(rand(), true).$key.microtime()), 0, SALT_LENGTH);
    return $salt;
}
$salt = createSalt()
//More prep for entering into table...

Then generate the hash with random salt:

$hash = hash('sha256', $salt . $pw); //$pw is the cleaned user submitted password

When the user logs in, compare the stored hash using the stored randomly generated salt:

$loginHash = hash('sha256', $dbSalt . $pw);
if ($loginHash == $dbHash) {
    //Logged in
} else {
    //Failed
}

How does that look?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

扭转时空 2024-10-17 13:37:41

我的两个提示:

  • 不要使用斜杠或魔术引号来防止 SQL 注入。使用 PDO 参数。没有例外。
  • 为每个用户使用不同的盐。盐不是秘密,将其与用户记录一起存储在数据库中。对每个人使用相同的盐会使您的数据库更容易受到攻击。

不相关但经常被忽视:不要限制用户的密码长度。我见过太多网站对密码长度施加任意限制(例如 12 个字符),但却有可笑的复杂性规则(“至少一个大写字母、一个小写字母、一个数字和一个特殊字符,但不是 ' <'、'>'" 或此类废话)。这是非常敌对的,避免它。

Two tips from me:

  • Don't use stripslashes or magic quotes to prevent SQL injection. Use PDO parameters. No exceptions.
  • Use a different salt for every user. The salt is no secret, store it in the DB along with the user record. Using the same salt for everyone makes your database more attackable.

Unrelated, but often overlooked: Do not limit password length for your users. I've seen too many web sites that impose an arbitrary limit (like 12 characters) on password length, but then have laughable complexity rules ("at least one upper, one lower, a digit and a special character but not '<', '>'" or such nonsense). This is very hostile, avoid it.

欲拥i 2024-10-17 13:37:41
  1. 您正在使用 MD5,它不再被视为“安全”。

  2. 你真的需要更长的盐。

为了有效地使用加盐,请

  1. You're using MD5, which isn't considered "safe" anymore.

  2. You really need a longer salt.

For effectively using salting, please see this question.

っ〆星空下的拥抱 2024-10-17 13:37:41

你不应该禁止 - 只要在 2 次失败尝试后输入验证码,但如果你真的想使用 memcache 来存储 IP。设置( $ip, true, false, $secondsTTL );稍后您可以使用 get( $ip ) 检查 - 将 TTL 设置为 2 小时。
您可能还想将所有内容放入函数中,并找出您更想用于字符串引号或双引号的内容。 ;)

总而言之,看起来它可以工作,但它真的很难阅读,而且有些东西是多余的。

you shouldnt ban - just put in a recaptcha after 2 failed attempts but if you really want to use memcache to store the ip. set( $ip, true, false, $secondsTTL ); later on you check with get( $ip ) - set the TTL to 2 hours.
you might also want to put everything in functions and find out what you rather want to use for strings quote or double quote. ;)

all together it looks like it would work but its really hard to read and some stuff is redundant.

玩物 2024-10-17 13:37:41

clean()中,您应该添加mysql_real_escape_string()

根据您的服务器和其他设置,$_SERVER['REMOTE_ADDR'] 可以为空。

我建议创建一个简单的重定向函数,以防止在 header() 重定向后忘记 exit 时出现错误。

例子:

function redirect($url) {
  if (!headers_sent()) {
     header('Location: '.$url);
  } else {
     // echo $url;
  }
  exit;
}

In clean() you should add mysql_real_escape_string().

Depending on you server and other settings, $_SERVER['REMOTE_ADDR'] can be empty.

I would recommend to create a simple redirect function to prevent errors when forgetting the exit after a header() redirect.

Example:

function redirect($url) {
  if (!headers_sent()) {
     header('Location: '.$url);
  } else {
     // echo $url;
  }
  exit;
}
会傲 2024-10-17 13:37:40

好的,这里有一些:

  1. 您不应该再使用 md5。您可以,但还有更好的方法(例如与hash() 函数)。

  2. 我也会使用更长的静态盐。我建议至少 64 个字符(毕竟,写入时计算的开销最小,但更难以猜测)。

  3. 我还会添加动态(随机)盐。为每个用户生成一个新密码,并将其与密码散列一起存储(通常用 : 字符分隔它们)。这样,即使您的静态盐受到损害,也需要为数据库中的每个密码生成(或至少迭代)一个新的彩虹表...

  4. 不要信任或基于 IP 地址进行操作任何非时间性的东西。大多数 ISP 使用一种 NAT 形式,从一个 IP 可以看到多个用户(随着 IPv4 命名空间的耗尽,这种情况只会变得更加普遍)。如果您想限制速率或暂时阻止 IP 地址,没问题。但不要禁止它们...

  5. 你的clean()函数应该首先检查字符串,或者强制它是一个字符串:($str = is_string( $str) ? 修剪($str) : (字符串) $str;)。它根本不能阻止sql注入。但是 stripslashes 调用是必要的(正是您所拥有的),以允许代码在设置了 magic_quotes_gpc 的服务器上工作(它将尝试为您转义引号)。 .. 所以保留它。

  6. 更好地格式化您的代码。创建函数来处理相关任务。这样您就不需要查看 75 行程序代码来弄清楚发生了什么。更好的是,将其包装在一个类中,并将常见任务(数据库访问等)移至它们自己的类中。并记住正确缩进。可读性是王道,所以不要走捷径...

编辑: 至于如何验证密码,您首先获取加盐哈希,然后使用存储的盐重新计算哈希。 (我在下面展示的 makeSaltedHash 函数额外使用了名为密钥拉伸的功能。

function validatePassword($password, $hash) {
    list($oldHash, $salt) = explode(':', $hash, 2);
    $newHash = makeSaltedHash($password, $salt);
    return $hash == $newHash;
}

function makeSaltedHash($password, $salt = '') {
    if (empty($salt)) {
        $salt = makeRandomSalt(mt_rand(64, 128));
    }
    $hash = hash('sha512', $password . $salt . SALT);
    for ($i = 0; $i < 50; $i++) {
        $hash = hash('sha512', $password . $salt . SALT . $hash);
    }
    return $hash . ':' . $salt;
}

function makeRandomSalt($length = 64) {
    $salt = '';
    for ($i = 0; $i < $lenght; $i++) {
        $salt .= chr(mt_rand(33, 126));
    }
    return $salt;
}

Ok, here's a few:

  1. You shouldn't be using md5 anymore. You can, but there are better methods (Such as sha512 used with the hash() function).

  2. I would use a MUCH longer static salt as well. I suggest at least 64 characters (after all, it's minimal overhead to compute on write, but much more difficult to guess).

  3. I would also add a dynamic (random) salt as well. Generate a new one for each user, and store it along side the password hash (it's common to separate them by a : character). This way, even if your static salt is compromised, a new rainbow table would need to be generated (or at least iterated against) for each password in your db...

  4. Don't trust or operate based upon IP address for anything non-temporal. Most ISPs use a form of NAT where more than one user will be seen from a single IP (and this will only become more prevalent with the exhaustion of the IPv4 namespace). If you want to rate-limit or temporarily block IP addresses, fine. But don't ban on them...

  5. Your clean() function should either check for a string first, or force it to be a string: ($str = is_string($str) ? trim($str) : (string) $str;). It doesn't prevent sql injection at all. But the stripslashes call is necessary (exactly how you have it) to allow the code to work on servers that have magic_quotes_gpc set (which will attempt to escape quotes for you)... So keep that around.

  6. Format your code better. Create functions to handle related tasks. That way you don't have 75 lines of procedural code to look through to figure out what's going on. Better yet, wrap it in a class and move the common tasks (db access, etc) into their own classes. And remember to indent properly. Readability is king, so don't take shortcuts...

Edit: As far as how to validate the password, you fetch the salted hash first, then re-compute the hash with the stored salt. (The makeSaltedHash function I show below makes additional use of something called Key Stretching.

function validatePassword($password, $hash) {
    list($oldHash, $salt) = explode(':', $hash, 2);
    $newHash = makeSaltedHash($password, $salt);
    return $hash == $newHash;
}

function makeSaltedHash($password, $salt = '') {
    if (empty($salt)) {
        $salt = makeRandomSalt(mt_rand(64, 128));
    }
    $hash = hash('sha512', $password . $salt . SALT);
    for ($i = 0; $i < 50; $i++) {
        $hash = hash('sha512', $password . $salt . SALT . $hash);
    }
    return $hash . ':' . $salt;
}

function makeRandomSalt($length = 64) {
    $salt = '';
    for ($i = 0; $i < $lenght; $i++) {
        $salt .= chr(mt_rand(33, 126));
    }
    return $salt;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文