请批评这个 PHP 登录脚本
我开发的一个网站最近遭到破坏,很可能是受到暴力攻击或彩虹表攻击。原始登录脚本没有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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
我的两个提示:
不相关但经常被忽视:不要限制用户的密码长度。我见过太多网站对密码长度施加任意限制(例如 12 个字符),但却有可笑的复杂性规则(“至少一个大写字母、一个小写字母、一个数字和一个特殊字符,但不是 ' <'、'>'" 或此类废话)。这是非常敌对的,避免它。
Two tips from me:
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.
您正在使用 MD5,它不再被视为“安全”。
你真的需要更长的盐。
为了有效地使用加盐,请
You're using MD5, which isn't considered "safe" anymore.
You really need a longer salt.
For effectively using salting, please see this question.
你不应该禁止 - 只要在 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.
在
clean()
中,您应该添加mysql_real_escape_string()
。根据您的服务器和其他设置,
$_SERVER['REMOTE_ADDR']
可以为空。我建议创建一个简单的重定向函数,以防止在
header()
重定向后忘记exit
时出现错误。例子:
In
clean()
you should addmysql_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 aheader()
redirect.Example:
好的,这里有一些:
您不应该再使用
md5
。您可以,但还有更好的方法(例如与hash()
函数)。我也会使用更长的静态盐。我建议至少 64 个字符(毕竟,写入时计算的开销最小,但更难以猜测)。
我还会添加动态(随机)盐。为每个用户生成一个新密码,并将其与密码散列一起存储(通常用
:
字符分隔它们)。这样,即使您的静态盐受到损害,也需要为数据库中的每个密码生成(或至少迭代)一个新的彩虹表...不要信任或基于 IP 地址进行操作任何非时间性的东西。大多数 ISP 使用一种 NAT 形式,从一个 IP 可以看到多个用户(随着 IPv4 命名空间的耗尽,这种情况只会变得更加普遍)。如果您想限制速率或暂时阻止 IP 地址,没问题。但不要禁止它们...
你的
clean()
函数应该首先检查字符串,或者强制它是一个字符串:($str = is_string( $str) ? 修剪($str) : (字符串) $str;
)。它根本不能阻止sql注入。但是stripslashes
调用是必要的(正是您所拥有的),以允许代码在设置了magic_quotes_gpc
的服务器上工作(它将尝试为您转义引号)。 .. 所以保留它。更好地格式化您的代码。创建函数来处理相关任务。这样您就不需要查看 75 行程序代码来弄清楚发生了什么。更好的是,将其包装在一个类中,并将常见任务(数据库访问等)移至它们自己的类中。并记住正确缩进。可读性是王道,所以不要走捷径...
编辑: 至于如何验证密码,您首先获取加盐哈希,然后使用存储的盐重新计算哈希。 (我在下面展示的
makeSaltedHash
函数额外使用了名为密钥拉伸的功能。Ok, here's a few:
You shouldn't be using
md5
anymore. You can, but there are better methods (Such assha512
used with thehash()
function).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).
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...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...
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 thestripslashes
call is necessary (exactly how you have it) to allow the code to work on servers that havemagic_quotes_gpc
set (which will attempt to escape quotes for you)... So keep that around.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.