¿Es esta área de administración lo suficientemente segura? V2

5

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:

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.Consulte ejemplo 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>&nbsp;
<input class="textinput" type="text" name="uid"/><br>
<div class="label">Password:</div>&nbsp;
<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>
    
pregunta Community 26.04.2012 - 23:30
fuente

1 respuesta

4

En general. Esto es mucho mejor. Pero todavía tengo algunos comentarios -

Clickjacking. Tu diagrama para prevenir el clickjacking es muy vago. Supongo que estás haciendo framebusting del lado del cliente (usando Javascript que se ejecuta en el cliente). Tenga en cuenta que esto es extremadamente propenso a errores y la mayoría de las personas que escriben su propio código de creación de marcos terminan con algo que está sutilmente roto. Un documento académico examinó los 500 sitios más utilizados (en 2010) y descubrió que todos los sitios que usaban framebusting, lo implementaron de una manera insegura . Los ataques fueron sutiles y no obvios, por lo que los desarrolladores probablemente pensaron que su código estaba bien, cuando en realidad tenía fallas.

Le sugiero que lea el siguiente documento, que describe los enfoques que son defectuosos y también describe cómo hacer el framebusting correctamente:

Cumplimiento del flujo de trabajo. No entendí su discusión acerca de garantizar que no se pueda acceder a checkout.php antes de review.php . No me queda claro por qué enumera esto como una propiedad de seguridad o qué valor de seguridad agrega. No me suena como un problema de seguridad. Además, su mecanismo puede romperse al tener varias copias de su sitio abiertas en varias pestañas.

Si su preocupación es sobre ataques de navegación forzada, sugeriría simplemente asegurarse de que utiliza la defensa CSRF adecuada en todos los lugares del sitio, incluida la página checkout.php y todos los destinos de formulario en esa página.

Defensa de CSRF. Tenga en cuenta que necesita usar un token de CSRF para proteger todas las acciones de efecto secundario. Si su sitio está correctamente diseñado, esto significa que debe usarse un token CSRF para proteger todas las solicitudes POST.

Tokens CSRF. Utiliza mt_rand() para generar el token CSRF. Esto no es seguro y representa una mala práctica. Debe usar un PRNG criptográficamente fuerte (por ejemplo, leer de /dev/urandom ), para asegurarse de que el atacante no pueda predecir este valor. mt_rand() no es criptográficamente fuerte. Consulte, por ejemplo, ¿Es seguro un rand del rand de glibc para una clave de inicio de sesión? , ¿Cuáles son los requisitos para que un generador de números aleatorios sea seguro de usar en criptografía? , ¿Es seguro un rand de / dev / urandom para una clave de inicio de sesión? .

Además, estás llamando a mt_rand(5,15) , lo que significa que tu número aleatorio es del rango 5..15. En otras palabras, tienes menos de 4 bits de aleatoriedad. Esto es totalmente inadecuado y completamente roto. Esperemos que haya sido un error tipográfico, o tal vez no entendí bien.

Inicios de sesión fallidos. Prohibes al usuario durante un mes después de 10 intentos de inicio de sesión fallidos. Probablemente, esta no sea una gran idea, ya que hace que sea demasiado fácil para los "afligidos" bloquear a alguien de su cuenta por un mes. Sugiero leer las siguientes preguntas en este sitio: ¿Por qué los sitios implementan el bloqueo después de 3 intentos de contraseña fallidos? , ¿Está denegado el inicio de sesión después de que los intentos incorrectos no sean efectivos? , ¿Una estrategia adecuada para evitar el forzamiento brutal de inicios de sesión? , ¿Cómo aseguro mi página de inicio de sesión? , ¿Por qué volver a verificar con CAPTCHA en la entrada del formulario fallido? .

Cookies. Asegúrate de establecer la marca secure en todas las cookies (para que solo se envíen a través de conexiones HTTPS). Le sugiero que habilite la Seguridad de transporte estricta de HTTP para evitar ataques de estilo sslstrip.

Comentario general. Muchas de las cosas que estás construyendo son estándar de bog. En el futuro, podría considerar elegir un marco de desarrollo de aplicaciones web que brinde soporte para estos mecanismos (por ejemplo, gestión segura de sesiones, defensa CSRF, verificaciones de autorización). También lo invito a consultar los recursos de OWASP sobre seguridad de aplicaciones web. Cubren una gran cantidad de temas sobre los que estás preguntando.

    
respondido por el D.W. 28.04.2012 - 09:26
fuente

Lea otras preguntas en las etiquetas