¿Cómo puedo almacenar las contraseñas de mis usuarios de forma segura?

169

¿Cuánto más seguro es esto que el MD5 simple ? Acabo de comenzar a buscar seguridad de contraseña. Soy bastante nuevo en PHP.

$salt = 'csdnfgksdgojnmfnb';

$password = md5($salt.$_POST['password']);
$result = mysql_query("SELECT id FROM users
                       WHERE username = '".mysql_real_escape_string($_POST['username'])."'
                       AND password = '$password'");

if (mysql_num_rows($result) < 1) {
    /* Access denied */
    echo "The username or password you entered is incorrect.";
} 
else {
    $_SESSION['id'] = mysql_result($result, 0, 'id');
    #header("Location: ./");
    echo "Hello $_SESSION[id]!";
}
Barras de refuerzo
fuente
Tenga en cuenta que php 5.4+ tiene esto incorporado
Benjamin Gruenbaum
Consulte también el marco de hashing de contraseñas PHP de Openwall (PHPass). Es portátil y reforzado contra una serie de ataques comunes a las contraseñas de los usuarios.
jww
1
Obligatorio "utilizar PDO en lugar de interpolación de cadenas", para las personas que hoy en día tropiezan con esta pregunta.
Financia la demanda de Mónica

Respuestas:

270

La forma más fácil de asegurar su esquema de almacenamiento de contraseña es mediante el uso de una biblioteca estándar .

Debido a que la seguridad tiende a ser mucho más complicada y con más posibilidades invisibles de arruinar que la mayoría de los programadores podrían enfrentar solos, usar una biblioteca estándar es casi siempre la opción más fácil y segura (si no la única) disponible.


La nueva API de contraseña PHP (5.5.0+)

Si está utilizando PHP versión 5.5.0 o posterior, puede utilizar la nueva API simplificada de hashing de contraseñas

Ejemplo de código usando la API de contraseña de PHP:

<?php
// $hash is what you would store in your database
$hash = password_hash($_POST['password'], PASSWORD_DEFAULT, ['cost' => 12]);

// $hash would be the $hash (above) stored in your database for this user
$checked = password_verify($_POST['password'], $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}

(En caso de que todavía esté utilizando el legado 5.3.7 o posterior, puede instalar ircmaxell / password_compat para tener acceso a las funciones integradas)


Mejora en hash salados: agregue pimienta

Si desea seguridad adicional, la gente de seguridad ahora (2017) recomienda agregar un ' pimiento ' a los hashes de contraseña (automáticamente) salados.

Hay una caída simple en la clase que implementa de manera segura este patrón, recomiendo: Netsilik / PepperedPasswords ( github ).
Viene con una licencia MIT, por lo que puede usarla como quiera, incluso en proyectos propietarios.

Ejemplo de código usando Netsilik/PepperedPasswords:

<?php
use Netsilik/Lib/PepperedPasswords;

// Some long, random, binary string, encoded as hexadecimal; stored in your configuration (NOT in your Database, as that would defeat the entire purpose of the pepper).
$config['pepper'] = hex2bin('012345679ABCDEF012345679ABCDEF012345679ABCDEF012345679ABCDEF');

$hasher = new PepperedPasswords($config['pepper']);

// $hash is what you would store in your database
$hash = $hasher->hash($_POST['password']);

// $hash would be the $hash (above) stored in your database for this user
$checked = $hasher->verify($_POST['password'], $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}


La biblioteca estándar ANTIGUA

Tenga en cuenta: ¡ ya no debería necesitar esto! Esto es solo aquí para fines históricos.

Eche un vistazo a: Marco de hash de contraseña PHP portátil : phpass y asegúrese de usar el CRYPT_BLOWFISHalgoritmo si es posible.

Ejemplo de código usando phpass (v0.2):

<?php
require('PasswordHash.php');

$pwdHasher = new PasswordHash(8, FALSE);

// $hash is what you would store in your database
$hash = $pwdHasher->HashPassword( $password );

// $hash would be the $hash (above) stored in your database for this user
$checked = $pwdHasher->CheckPassword($password, $hash);
if ($checked) {
    echo 'password correct';
} else {
    echo 'wrong credentials';
}

PHPass se ha implementado en algunos proyectos bastante conocidos:

  • phpBB3
  • WordPress 2.5+ y bbPress
  • la versión Drupal 7 (módulo disponible para Drupal 5 y 6)
  • otros

Lo bueno es que no necesita preocuparse por los detalles, esos detalles han sido programados por personas con experiencia y revisados ​​por muchas personas en Internet.

Para obtener más información sobre los esquemas de almacenamiento de contraseñas, lea la publicación del blog de Jeff : Probablemente esté almacenando contraseñas incorrectamente

Hagas lo que hagas si eliges el enfoque ' Lo haré yo mismo, gracias ', no lo uses MD5ni SHA1más . Son buenos algoritmos de hash, pero se consideran rotos por motivos de seguridad .

Actualmente, usar crypt , con CRYPT_BLOWFISH es la mejor práctica.
CRYPT_BLOWFISH en PHP es una implementación del hash Bcrypt. Bcrypt se basa en el cifrado de bloques Blowfish, haciendo uso de su costosa configuración de teclas para ralentizar el algoritmo.

Jacco
fuente
29

Sus usuarios estarán mucho más seguros si utiliza consultas parametrizadas en lugar de concatenar sentencias SQL. Y la sal debe ser única para cada usuario y debe almacenarse junto con el hash de contraseña.

Anton Gogolev
fuente
1
Hay un buen artículo sobre seguridad en PHP en Nettuts +, también se menciona la salificación de contraseñas. Tal vez deberías echar un vistazo a: net.tutsplus.com/tutorials/php/…
Fábio Antunes
3
El Nettuts + es un artículo muy malo para usar como modelo: incluye el uso de MD5, que puede ser forzado con fuerza bruta incluso con sal. En su lugar, solo use la biblioteca PHPass, que es mucho, mucho mejor que cualquier código que pueda encontrar en un sitio de tutoría, es decir, esta respuesta: stackoverflow.com/questions/1581610/…
RichVel
11

Una mejor manera sería que cada usuario tenga una sal única.

El beneficio de tener sal es que dificulta que un atacante genere previamente la firma MD5 de cada palabra del diccionario. Pero si un atacante se entera de que tiene una sal fija, podría generar previamente la firma MD5 de cada palabra del diccionario con el prefijo de su sal fija.

Una mejor manera es que cada vez que un usuario cambia su contraseña, su sistema genera una sal aleatoria y la almacena junto con el registro del usuario. Es un poco más costoso verificar la contraseña (ya que necesita buscar la sal antes de poder generar la firma MD5) pero hace que sea mucho más difícil para un atacante pregenerar MD5.

R Samuel Klatchko
fuente
3
Las sales generalmente se almacenan junto con el hash de contraseña (por ejemplo, la salida de la crypt()función). Y dado que debe recuperar el hash de contraseña de todos modos, el uso de una sal específica del usuario no hará que el procedimiento sea más costoso. (¿O quiso decir que generar una nueva sal aleatoria es costoso? Realmente no lo creo). De lo contrario, +1.
Inshallah
Para fines de seguridad, es posible que desee proporcionar acceso a la tabla solo a través de procedimientos almacenados y evitar que se devuelva el hash. En cambio, el cliente pasa lo que cree que es el hash y obtiene un indicador de éxito o falla. Esto permite que el proceso almacenado registre el intento, cree una sesión, etc.
Steven Sudit
@Inshallah: si todos los usuarios tienen la misma sal, puede reutilizar el ataque de diccionario que usa en el usuario1 contra el usuario2. Pero si cada usuario tiene una sal única, deberá generar un nuevo diccionario para cada usuario que desee atacar.
R Samuel Klatchko
@R Samuel: esa es exactamente la razón por la que voté su respuesta, porque recomienda la estrategia de mejores prácticas para evitar tales ataques. Mi comentario tenía la intención de expresar mi perplejidad acerca de lo que dijo con respecto al costo adicional de una sal por usuario, que no entendí en absoluto. (dado que "las sales generalmente se almacenan junto con el hash de contraseña", cualquier almacenamiento adicional y requisitos de CPU para una sal por usuario son tan microscópicos que ni siquiera necesitan mencionarse ...)
Inshallah
@Inshallah: estaba pensando en el caso en el que se verificó la base de datos si la contraseña con hash está bien (entonces tiene una recuperación de db para obtener la sal y un segundo acceso de db para verificar la contraseña hash). Tiene razón sobre el caso en el que descarga la contraseña de sal / hash en una sola recuperación y luego hace la comparación en el cliente. Perdón por la confusion.
R Samuel Klatchko
11

Con PHP 5.5 (lo que describo está disponible incluso para versiones anteriores, ver más abajo) a la vuelta de la esquina, me gustaría sugerir que use su nueva solución incorporada: password_hash()y password_verify(). Proporciona varias opciones para lograr el nivel de seguridad de contraseña que necesita (por ejemplo, especificando un parámetro de "costo" a través de la $optionsmatriz)

<?php
var_dump(password_hash("my-secret-password", PASSWORD_DEFAULT));

$options = array(
    'cost' => 7, // this is the number of rounds for bcrypt
    // 'salt' => 'TphfsM82o1uEKlfP9vf1f', // you could specify a salt but it is not recommended
);
var_dump(password_hash("my-secret-password", PASSWORD_BCRYPT, $options));
?>

volverá

string(60) "$2y$10$w2LxXdIcqJpD6idFTNn.eeZbKesdu5y41ksL22iI8C4/6EweI7OK."
string(60) "$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d."

Como puede ver, la cadena contiene la sal, así como el costo que se especificó en las opciones. También contiene el algoritmo utilizado.

Por lo tanto, al verificar la contraseña (por ejemplo, cuando el usuario inicia sesión), al usar la password_verify()función complementaria , extraerá los parámetros criptográficos necesarios del hash de la contraseña.

Cuando no se especifica una sal, el hash de contraseña generado será diferente en cada llamada password_hash()porque la sal se genera aleatoriamente. Por lo tanto, la comparación de un hash anterior con uno recién generado fallará, incluso para una contraseña correcta.

Verificación funciona así:

var_dump(password_verify("my-secret-password", '$2y$10$BjHJbMCNWIJq7xiAeyFaHOGaO0jjNoE11e0YAer6Zu01OZHN/gk6K'));
var_dump(password_verify("wrong-password", '$2y$10$BjHJbMCNWIJq7xiAeyFaHOGaO0jjNoE11e0YAer6Zu01OZHN/gk6K'));

var_dump(password_verify("my-secret-password", '$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d.'));
var_dump(password_verify("wrong-password", '$2y$07$TphfsM82o1uEKlfP9vf1fOKohBqGVXOJEmnUtQu7Y1UMft1R4D3d.'));

Espero que proporcionar estas funciones integradas pronto proporcione una mejor seguridad de contraseña en caso de robo de datos, ya que reduce la cantidad de pensamiento que el programador tiene que poner en una implementación adecuada.

Hay una pequeña biblioteca (un archivo PHP) que le dará PHP 5.5 password_hashen PHP 5.3.7+: https://github.com/ircmaxell/password_compat

akirk
fuente
2
En la mayoría de los casos, es mejor omitir el parámetro de sal. La función crea una sal a partir de la fuente aleatoria del sistema operativo, hay muy pocas posibilidades de que pueda proporcionar una mejor sal por su cuenta.
martinstoeckli
1
Eso es lo que escribí, ¿no? "si no se especifica sal, se genera aleatoriamente, por eso es preferible no especificar una sal"
akirk
La mayoría de los ejemplos muestran cómo agregar ambos parámetros, incluso cuando no se recomienda agregar una sal, así que me pregunto por qué. Y para ser honesto, leí solo el comentario detrás del código, no en la siguiente línea. De todos modos, ¿no sería mejor cuando el ejemplo muestra cómo usar mejor la función?
martinstoeckli
Tienes razón, estoy de acuerdo. He cambiado mi respuesta en consecuencia y he comentado la línea. Gracias
akirk
¿Cómo debo verificar si la contraseña guardada y la contraseña ingresada son las mismas? Estoy usando password_hash()y password_verifyno importa qué contraseña (correcta o no) usé, termino con la contraseña correcta
Brownman Revival
0

Eso está bien para mí. El Sr. Atwood escribió sobre la fuerza de MD5 contra las tablas del arco iris , y básicamente con una sal larga como esa estás sentado bonito (aunque algunos signos de puntuación / números aleatorios, podrían mejorarlo).

También puedes mirar SHA-1, que parece ser cada vez más popular en estos días.

nickf
fuente
66
La nota en la parte inferior de la publicación del Sr. Atwood (en rojo) se vincula con otra publicación de un profesional de seguridad que dice que usar MD5, SHA1 y otros hashes rápidos para almacenar contraseñas es muy incorrecto.
sipwiz
2
@Matthew Scharley: No estoy de acuerdo con que el esfuerzo adicional impuesto por los costosos algoritmos de hashing de contraseñas sea una falsa seguridad. Es para protegerse de la fuerza bruta de contraseñas fácilmente adivinables. Si está limitando los intentos de inicio de sesión, entonces está protegiendo contra lo mismo (aunque un poco más eficaz). Pero si un adversario tiene acceso a los hashes almacenados de la base de datos, podrá forzar la fuerza bruta de tales contraseñas (fácilmente adivinables) con bastante rapidez (dependiendo de cuán fácil de adivinar). El valor predeterminado para el algoritmo de cripta SHA-256 es 10000 ronda, por lo que sería 10000 veces más difícil.
Inshallah
3
Los hash lentos en realidad se realizan iterando uno rápido una gran cantidad de veces y barajando los datos entre cada iteración. El objetivo es garantizar que, incluso si el chico malo obtiene una copia de los valores hash de su contraseña, tiene que quemar una cantidad considerable de tiempo de CPU para probar su diccionario con sus valores hash.
caf
44
@caf: creo que el algoritmo bcrypt hace uso del costoso parametrizable de la programación de claves de Eksblowfish; no estoy del todo seguro de cómo funciona, pero la programación de claves suele ser una operación muy costosa que se realiza durante el inicio de un objeto de contexto de cifrado, antes de que se realice cualquier cifrado.
Inshallah
3
Inshallah: Esto es cierto: el algoritmo bcrypt es un diseño diferente, donde la cripto primitiva subyacente es un cifrado de bloque en lugar de una función hash. Me refería a esquemas basados ​​en funciones hash, como la cripta MD5 de PHK ().
caf
0

Quiero agregar:

  • No limite las contraseñas de los usuarios por longitud

Por compatibilidad con sistemas antiguos, a menudo se establece un límite para la longitud máxima de la contraseña. Esta es una mala política de seguridad: si establece una restricción, configúrela solo para la longitud mínima de las contraseñas.

  • No envíe contraseñas de usuario por correo electrónico.

Para recuperar una contraseña olvidada, debe enviar la dirección por la cual el usuario puede cambiar la contraseña.

  • Actualiza los hash de las contraseñas de los usuarios

El hash de contraseña puede estar desactualizado (los parámetros del algoritmo pueden actualizarse). Al usar la función password_needs_rehash(), puede verificarlo.


fuente