Por qué la autenticación "hecha en casa" es una bomba de tiempo
\n\nCada segundo desarrollador PHP ha escrito su propio sistema de inicio de sesión al menos una vez. Parece sencillo: un formulario, verificación de usuario, una sesión. Pero es precisamente aquí, en esta etapa "simple", donde millones de sitios web perecen. Inyecciones SQL, robo de sesiones, fuerza bruta de contraseñas: ataques que convierten su proyecto en un filtro de datos personales.
\n\nEn este artículo analizaremos la autenticación segura en PHP desde cero. Sin magia: solo prácticas comprobadas utilizadas en Facebook, GitHub y su framework favorito. Aprenderá cómo hacer hash de contraseñas correctamente, proteger las sesiones contra la interceptación e implementar mecanismos modernos como JWT.
\n\n¿Listo? Comencemos.
\n\n1. Hash de contraseñas: por qué md5() es una sentencia de muerte
\n\nLa primera regla de seguridad: nunca, jamás almacene contraseñas en texto plano. La segunda regla: no use algoritmos obsoletos como MD5 o SHA1. Se calculan en microsegundos, y cualquier estudiante con un portátil descifrará su contraseña en una hora.
\n\nPHP tiene una solución integrada: las funciones password_hash() y password_verify(). Utilizan bcrypt (o Argon2, si PHP ≥ 7.2) y agregan automáticamente una "sal": una cadena aleatoria que hace que cada hash sea único.
Ejemplo de hash correcto
\n// Registro de usuario\n$password = $_POST['password'];\n$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);\n\n// Guardamos $hash en la base de datos\n// ¡Sin sal manual! PHP lo hace todo solo.\n\n\nVerificación de contraseña al iniciar sesión
\n// Inicio de sesión de usuario\n$inputPassword = $_POST['password'];\n$storedHash = 'de la base de datos';\n\nif (password_verify($inputPassword, $storedHash)) {\n // Contraseña correcta — permitimos acceso\n} else {\n // Contraseña incorrecta\n}\n\n\nObserve el parámetro cost. Define el "coste" de calcular el hash. Cuanto mayor sea el número, más tiempo tardará un atacante en descifrar la contraseña. Para 2024, cost = 12 es el punto óptimo entre seguridad y velocidad.
2. Sesiones: cómo no entregar las llaves de la casa
\n\nDespués de un inicio de sesión exitoso, normalmente iniciamos una sesión. Pero las sesiones estándar de PHP tienen puntos débiles: IDs predecibles, fugas a través de URL, fijación de sesión (session fixation).
\n\nConfiguración segura de sesión
\n// Al principio del script, antes de cualquier salida HTML\nini_set('session.use_only_cookies', 1);\nini_set('session.use_strict_mode', 1);\nini_set('session.cookie_httponly', 1);\nini_set('session.cookie_secure', 1); // Solo para HTTPS\nini_set('session.cookie_samesite', 'Lax');\n\nsession_start();\n\n// Regeneramos el ID de sesión después del inicio de sesión\nif ($login_success) {\n session_regenerate_id(true);\n $_SESSION['user_id'] = $user['id'];\n}\n\n\n¿Qué hemos hecho?
\n- \n
- use_only_cookies — prohibimos la transmisión del ID de sesión a través de URL (para que no sea robado de un enlace) \n
- use_strict_mode — bloqueamos sesiones no inicializadas \n
- httponly — prohibimos el acceso de JavaScript a la cookie de sesión (protección contra XSS) \n
- secure — la cookie se transmite solo a través de HTTPS \n
- samesite — protección contra ataques CSRF mediante falsificación de solicitudes entre sitios \n
Y lo más importante: siempre regenere el ID de sesión después del inicio de sesión. De lo contrario, un atacante podría proporcionar previamente su propio ID de sesión al usuario e interceptar la cuenta.
\n\n3. Protección contra CSRF: verificar que la solicitud fue enviada por usted
\n\nCSRF (Cross-Site Request Forgery) es un ataque en el que un usuario hace clic en un enlace como /delete-account?id=1 y, sin querer, ejecuta una acción. Para evitarlo, use tokens.
Generación y verificación del token CSRF
\n// Generación del token (al inicio de la sesión o al cargar el formulario)\nif (empty(