允许密码重置的问题在于其中存在严重的安全漏洞。
因为在我的文件中reset.php
,它发送的帐户激活码与用户注册时生成的帐户激活码相同。
对于攻击者来说,很容易进行多次尝试,例如以这种方式:
example.com/login-system/reset.php?email=ponercualquieremail%key=generarcódigoaleatorio
如果它与表记录匹配users
,攻击者可以更改密码并获得访问系统的访问权限。
由于相同的激活码,所有这些安全漏洞。
所以,这个链接,重置密码:
http://example.com/login-system/reset.php?email=example%40gmail.com&key=523db8c57a3d17d0860fa705c4c24ec62efc0c68f2f1443e39938361424099f1
激活账号也是一样的:
http://example.com/login-system/verify.php?email=example%40gmail.com&key=523db8c57a3d17d0860fa705c4c24ec62efc0c68f2f1443e39938361424099f1
更重要的是,我可以保存我收到的用于激活帐户、重置密码的代码,而无需在重置表单中输入电子邮件password
,只需将其更改verify.php?
为reset.php?
现在我的表结构如下users
+----------+-----------+--------+----------+------------+--------+
| id_user | username | email | password | email_code | active |
+----------+-----------+--------------------------------+--------+
| 1 | karla | karla@ | $2y$10...| 23db8c5... | 1 |
+-------------+-------------+--------------+------------+---------
如何解决此安全问题,将该激活码连同用户 ID 发送到另一个表并附上过期时间,以及在激活帐户时删除记录,以及在请求重置密码时创建新的验证码到期日期和重置密码时,该记录被再次删除。
我的完整代码。
注册.php
session_start();
include "require.ini.php";
if (isset($_POST['formsubmitted'])) {
$msg = array();
if (empty($_POST['username'])) {
$msg[] = 'Por favor, ingrese un nombre de usuario';
} else {
$username = $_POST['username'];
}
if (empty($_POST['email'])) {
$msg[] = 'Por favor, ingrese su correo electrónico';
} else {
if (preg_match("/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/", $_POST['email'])) {
$email = $_POST['email'];
} else {
$msg[] = 'Tu dirección de correo electrónico no es válida';
}
}
if (strlen($_POST['password']) <6){
$msg[] = 'Su contraseña debe tener al menos 6 caracteres';
}
if ($_POST['password'] !== $_POST['password_again']){
$msg[] = 'Su contraseña no coincide';
} else {
$password = $_POST['password'];
}
if (empty($_POST['firstname'])) {
$msg[] = 'Por favor, ingrese su nombre';
} else {
$first_name = $_POST['firstname'];
}
if (empty($msg)) {
$stmt = $con->prepare("SELECT * FROM users WHERE email=? OR username=?");
$stmt->bind_param("ss",$email,$username);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows>0) {
echo "¡El usuario con este correo electrónico ya existe!";
} else {
$hash_password = password_hash($password, CRYPT_BLOWFISH);
$key = bin2hex(openssl_random_pseudo_bytes(32));
//$key_two = bin2hex(random_bytes(32)); // Disponible apartir de PHP V.7
$active_default = 0;
$stmtA = $con->prepare("INSERT INTO users (username, email, password, first_name, email_code, active) VALUES (?, ?, ?, ?, ?, ?)");
$stmtA->bind_param("sssssi", $username,$email,$hash_password,$first_name,$key,$active_default);
if($stmtA->execute()){
echo 'El enlace de confirmación ha sido enviado por correo electrónico. ¡Por favor, haga clic en el enlace del mensaje para activar su cuenta!';
$to = $email;
$subject = "Por favor, verifique su cuenta.";
$message_body = 'Hola '.$first_name.',
¡Gracias por registrarte!
Estas aún solo paso de ser parte de nuestra comunidad.
Por favor, haga clic en este enlace para activar su cuenta:
http://example.com/login-system/verify.php?email='.urlencode($email).'&key='.$key.'';
mail($to, $subject, $message_body, 'From: [email protected]');
//header("location: index.php");
//exit;
} else {
echo "Ha ocurrido un error internamente, por favor, vuelva intertar enviar su solicitud más tarde";
}
}
} else {
foreach ($msg as $key => $values) {
echo ' <div>'.$values.'</div>';
}
}
}
重置.php
<?php
session_start();
include "require.php";
if (isset($_GET['email']) && preg_match('/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/', $_GET['email'])) {
$email = $_GET['email'];
}
if (isset($_GET['key']) && (strlen($_GET['key']) == 64)) {
$key = $_GET['key'];
}
if (isset($email) && isset($key)) {
//$email = $con->escape_string($_GET['email']);
//$key = $con->escape_string($_GET['key']);
$active_defaul = 1;
$stmt = $con->prepare("SELECT * FROM users WHERE email=? AND email_code=? AND active=?");
$stmt->bind_param("ssi",$email,$key,$active_defaul);
$stmt->execute();
$stmt->store_result();
//if ($result->num_rows == 0 )
if ($stmt->num_rows==0) {
//if ($stmt->num_rows>0) {
echo "¡Ingresó una URL inválida para restablecer la contraseña!";
} else {
echo '
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<form action="reset_password.php" method="post">
<label>New Password</label>
<input type="password" name="password" autocomplete="off"/>
<label>Confirm New Password</label>
<input type="password" name="password_again" autocomplete="off"/>
<input type="hidden" name="email" value="'.$email.'">
<input type="submit" name="form_reset" value="Guardar contraseña" />
</form>
</body>
</html>';
}
} else {
echo "¡Acceso denegado!";
}
?>
重置密码.php
session_start();
include "require.php";
if (isset($_POST['form_reset'])) {
$email = $_POST['email'];
$password = $_POST['password'];
$hash_password = password_hash($password, CRYPT_BLOWFISH);
$stmt = $con->prepare("UPDATE users SET password= ? WHERE email=? OR username=?");
$stmt->bind_param("sss", $hash_password,$email,$email);
if($stmt->execute()){
header("location: correcto.php");
} else {
header("location: error.php");
}
}
答案
这是一个概率,但考虑到您随机生成 32 个字节。从理论上讲,您有 4,294,967,296 分之一的机会做对,或者假设网络延迟为 2 ms 和一个同时连接,所有组合都可以在大约 100 天内进行测试。
随着时间的推移限制令牌的有效性足以减轻暴力攻击的影响(请参阅有关限制尝试的提示)。
这是正确的。有多种方式可以无意中访问信息:
?...
)。https
)。在这些情况下,限制令牌的有效期也有帮助。
限制令牌的生命周期
expiracion
对代码进行微小的更改,我建议在表格中至少添加一个字段,users
如下所示:此字段将存储电子邮件验证或重置代码到期的日期。
现在是时候更改 SQL 和 PHP 代码来为流程提供安全性了:
注册.php
重置.php
注意:当它值得时
IF(expiracion > NOW(), 0, 1)
返回1
(过期)。expiracion
NULL
重置密码.php
注意:在您的代码中,您似乎没有考虑到有人可以使用作为另一个用户的电子邮件地址的用户名进行注册。这可能会为该用户打开更改其他用户密码的大门。
限制尝试失败的次数
为了减轻暴力攻击的影响(对用户名和密码以及令牌),您应该记录所有失败的访问尝试。
这种解决方案增加了代码的复杂性,它需要至少一个新表进行维护(删除旧记录以免在遭受暴力攻击并节省数百万次尝试时MySQL中的空间不足)和正确的管理您应该在哪里检查是否已超出限制以及在哪里添加新的失败尝试。
尽管如此,实现此解决方案的最简单方法是创建一个名为的表,例如
intentos
:每次用户/密码、电子邮件/令牌等访问失败时,都会添加一条记录,如下所示:
分配
?
的值$_SERVER['REMOTE_ADDR']
。现在,最重要的是,在进行任何身份验证检查之前,请检查来自源 IP 的失败尝试次数,防止检查是否超过阈值。
要添加表内容的自动维护,在所述验证之前,可以使用以下方法删除旧记录
DATE_SUB()
:此指令将删除所有超过 30 秒的尝试日志。
此示例代码已集成到您的应用程序中:
限制为每 30 秒尝试 10 次会将尝试所有 32 字节密钥 (4,294,967,296) 组合所需的时间增加到 400 年以上。
首先分析案例应该创建一个存储以下数据的新表:
.
您将需要一个将行插入表的部分
password_resets
,并且要创建该部分,您应该检查要重置其密码的电子邮件是否存在。现在,一旦您输入带有重置代码的 URL,您将需要验证:
expire
如果满足条件,则必须删除相应的记录,并显示输入新密码的部分。
Ese grave problema de seguridad se soluciona, asegurandote de que el usuario nunca accede mediante
GET
, es decir escribiendo la direccion con los parametros. Debes de utilizar el objeto session dePHP
. Es decir coloca los datos de usuario en objeto session, la clave siempre encriptada te aconsejoSHA-256
o similar. Si alguien intenta acceder medianteGET
,PHP
bloquea al usuario.