Esta es una respuesta a mi pregunta anterior, ¿Es esta área de administración lo suficientemente segura? .
Hubo algunas respuestas muy útiles allí, por lo que estoy muy agradecido. Así que volví al tablero de dibujo y aquí está mi nueva pregunta, porque soy nuevo en el aspecto de la seguridad y no estoy seguro de haber comprendido o cubierto todos los agujeros de bucle correctamente .
Los problemas contra los que estoy tratando de proteger el sistema son:
- Ataques de Man in the Middle
- Ataques de inyección SQL
- Ataques CSRF
- ataques XSS
- Conectores de clic
- Ataques de fuerza bruta
Notas
- Las contraseñas se almacenan con esta implementación de bcrypt
- El sitio completo utilizará el protocolo https: //
- Indicadores session.cookie_secure y session.cookie_httponly ser establecido
- objeto PDO de PHP se utiliza en todas las consultas SQL, todas las entradas están parametrizadas
- Cualquier variable de entrada del usuario se eliminará
- Cada página comenzará con un encabezado http
x-frame-options: deny
- Cada página restringida comenzará con un script de autorización (ver fig. 1)
- Todas las formas solo se autorizarán si la variable establecida
$_SESSION["auth_key"]
coincide con la clave enviada con la forma (ver fig. 1)
Fig.1: Autorización & Script de inicio de sesión
Esperoqueestediagramaseaensumayoríaautoexplicativo.Sinembargo,intentaréaclararciertasfunciones.
- vereltoken:untokenquesegeneraunavezqueelusuarioiniciasesión,sealmacenaen
$_SESSION['auth_token']
yseenvíaconcualquierformularioqueelusuarioenvíe. - comprobarsesión:sielusuariotieneuntokenválido,extraigasusdetallesdelabasededatosutilizandoelPHPSESSID(serequiereelnombreparadarlabienvenidaalusuario).SinoseencuentraelIDdesesiónosieltokennoesválido,susesiónhacaducadoydebeniniciarsesiónnuevamente.
- regen.sesión:unavezqueelusuariohayainiciadosesióncorrectamente,proporcioneunnuevoIDdesesión(
session_regenerate_id()
)yactualicelabasededatosconelnuevoIDdesesión.Estableceel$_SESSION['auth_token']=md5(mt_rand(1,10000).$username);
.Elnombredeusuarioesúnico,porloquedebegeneraruntokendeautenticaciónúnicoparaquepuedanenviarsuspropiosformularios.Noestoysegurodehaberimplementadoestocorrectamente.Consulteejemplo aquí - comprobar inicio de sesión: simplemente verifica que la contraseña cifrada coincida con la entrada cifrada y los nombres de usuario coincidan.
- intentos fallidos: este lado se agrega para ralentizar los ataques de fuerza bruta; Primero se agrega un captcha, luego se bloquea temporalmente la IP y el usuario / titular de la cuenta recibe un correo electrónico de notificación.
- Añadiré más detalles si es necesario / corregiré estos detalles si hay algún error
Implementación (hasta ahora):
Página restringida: comienza con el requisito de authenticate.php
<?php require_once "authenticate.php"; ?>
<p>Welcome <?php echo $_SESSION["guest"]; ?>!
<a href="index.php?action=logout">Logout</a></p>
<p><a href="index.php?token=<?php echo $_SESSION["auth_token"] ?>">Try again.</a></p>
Authenticate.php: redirige a login.php
si no se autentica
Nota: Dejé el cifrado fuera (mientras realizaba la prueba) porque bcrypt
no está disponible en mi versión de PHP ( 5.2.17) así que estoy buscando SHA256 o SHA512.
<?php
session_start();
// load database abstraction layer
require_once "../../dal.php";
unset($_SESSION["login_errors"]);
function loginError($str) {
if (isset($_SESSION["login_errors"]))
$_SESSION["login_errors"] = "" . $_SESSION["login_errors"] . "; " . $str;
else
$_SESSION["login_errors"] = $str;
}
function hasValidToken() {
return (isset($_SESSION["auth_token"]) && isset($_GET["token"])
&& $_SESSION["auth_token"] == $_GET["token"]);
}
function hasValidSession() {
return (session_id()? hasOpenSession() : false);
}
function hasOpenSession() {
$sql = "SELECT * FROM tbl_store_admin WHERE php_sesskey=?;";
$data = array(session_id());
return (dbRowsCount($sql, $data) == 1);
}
function updateUserSession() {
$sql = "UPDATE tbl_store_admin SET php_sesskey=? WHERE username=?";
$data = array(session_id(), $_SESSION["uid"]);
dbQuery($sql, $data);
return (dbRowsAffected() == 1);
}
function hasLoggedIn() {
if (isset($_POST["uid"]) && isset($_POST["key"])) {
$uid = htmlspecialchars($_POST["uid"]);
$key = htmlspecialchars($_POST["key"]);
return (getUser($uid, $key));
} else {
return false;
}
}
function wrongCredentials() {
if (isset($_SESSION["login_attempts"])) {
$_SESSION["login_attempts"] = $_SESSION["login_attempts"]+1;
} else {
$_SESSION["login_attempts"] = 1;
}
logout();
loginError("Bad Credentials");
return false;
}
function getUser($uid, $key) {
$sql = "SELECT * FROM tbl_store_admin WHERE username=? AND keycode=? LIMIT 1;";
$data = array($uid, $key);
$rows = dbRowsCount($sql, $data);
if ($rows == 1) {
dbQuery($sql, $data);
$user = dbFetch();
// store data in session
$_SESSION["uid"] = $user["username"];
$_SESSION["guest"] = $user["nickname"];
return true;
} else {
return wrongCredentials();
}
}
function logout() {
if (isset($_SESSION["auth_token"])) {
unset($_SESSION["auth_token"]);
}
if (isset($_SESSION["uid"])) {
$sql = "UPDATE tbl_store_admin SET php_sesskey=NULL WHERE username=?;";
$data = array($_SESSION["uid"]);
dbQuery($sql, $data);
unset($_SESSION["uid"]);
}
if (isset($_SESSION["guest"])) {
unset($_SESSION["guest"]);
}
}
function hasAuthenticated() {
if (hasValidToken()) {
if (hasValidSession()) {
return true;
} else {
logout();
return false;
}
} else {
logout();
return hasLoggedIn();
}
}
function regenerateSession() {
session_regenerate_id();
updateUserSession();
$token = "" . mt_rand(1,10000) . $_SESSION["uid"];
$_SESSION["auth_token"] = md5($token);
}
if (isset($_GET["action"]) && $_GET["action"]=="logout") {
logout();
}
if (!hasAuthenticated()) {
header('Location: login.php');
} else {
regenerateSession();
}
?>
Login.php: Un formulario de inicio de sesión simple
<?php
session_start();
function getRequestURL() {
if (isset($_SESSION['HTTP_REFERER'])) {
return $_SESSION['HTTP_REFERER'];
}
return "index.php?token=".$_SESSION["auth_token"];
}
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="X-Frame-Options" content="deny">
<title>Admin Login</title>
<style type="text/css">
.errorbox {
position:absolute;
top:0px;
left:0px;
width:100%;
height:100px;
background-color:#FFAAAA;
border:1px solid #FF0000;
}
.loginbox {
width:600px;
height:300px;
border:5px solid #000000;
margin:100px auto;
border-radius:10px;
-moz-border-radius:10px;
-webkit-border-radius:10px;
background-color:#CCCCCC;
box-shadow:4px 4px 2px #999999;
-moz-box-shadow:4px 4px 2px #999999;
-webkit-box-shadow:4px 4px 2px #999999;
}
.formbox {
width:400px;
height:200px;
margin:0px auto;
}
.label {
width:120px;
height:40px;
text-align:right;
line-height:40px;
float:left;
font-weight:bold;
}
.textinput {
height:40px;
width:260px;
font-size:30px;
line-height:40px;
}
.submitbtn {
height:50px;
width:100px;
font-size:30px;
line-height:40px;
}
</style>
</head>
<body style="font-family: Arial;font-size:20px;">
<?php
if (isset($_SESSION["login_errors"])) {
?>
<div class="errorbox">
Error: <?php echo ($_SESSION["login_errors"]); ?>
</div>
<?php
}
?>
<div class="loginbox">
<h1 align="center">Admin Area</h1>
<div class="formbox">
<form action="<?php echo getRequestURL(); ?>" method="POST">
<p><big>
<div class="label">Name:</div>
<input class="textinput" type="text" name="uid"/><br>
<div class="label">Password:</div>
<input class="textinput" type="password" name="key"/><br>
</big></p>
<p align="right"><input class="submitbtn" type="submit" value="Log In"/></p>
</form>
</div>
</div>
</body>
</html>