5 errores críticos que todos cometen con PHP (y cómo solucionarlos)
Breve y directo: estos cinco fallos aparecen con frecuencia en apps PHP y causan desde vulnerabilidades hasta fallos en producción. Para cada uno verás por qué es peligroso, un ejemplo 'malo' y la corrección recomendada.
1) Concatenar entradas en consultas SQL (inyección SQL)
Por qué duele: concatenar parámetros en queries permite a un atacante modificar la consulta y acceder o destruir datos.
// MAL: vulnerable a SQL injection
$u = $_POST['user'];
$q = "SELECT * FROM users WHERE username = '" . $u . "' AND active = 1";
$res = $db->query($q);
Solución: usa consultas preparadas (PDO) y nunca confíes en el input.
// BIEN: PDO con prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :u AND active = 1');
$stmt->execute([':u' => $_POST['user']]);
$user = $stmt->fetch();
Por qué: los parámetros se envían por separado y el driver se encarga del escape correcto.
2) Almacenar contraseñas con MD5/SHA1 o texto plano
Por qué duele: MD5/SHA1 son rápidos y no están diseñados para contraseñas; son vulnerables a ataques por diccionario y rainbow tables.
// MAL
$hash = md5($password);
// Y peor: guardar $password directamente en la DB
Solución: usa password_hash() y password_verify(), que aplican bcrypt/argon2 y manejan el salt automáticamente.
// BIEN
$hash = password_hash($password, PASSWORD_DEFAULT);
// Al verificar
if (password_verify($candidate, $hash)) {
// OK
}
Consejo: revisa password_needs_rehash() tras un cambio de algoritmo y actualiza hashes en el login.
3) Mostrar errores en producción
Por qué duele: mostrar stack traces o errores revela rutas, consultas y configuración del servidor a atacantes.
// MAL: en producción
ini_set('display_errors', 1);
error_reporting(E_ALL);
Solución: desactiva display_errors y registra errores en archivos o sistemas de logging. Detecta entorno (env) y ajusta comportamiento.
// BIEN: entorno controlado
if ($env === 'production') {
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/app/php_errors.log');
}
// Manejo simple de excepciones
set_exception_handler(function (Throwable $e) {
error_log($e->getMessage() . " in " . $e->getFile() . ':' . $e->getLine());
http_response_code(500);
echo 'Ocurrió un error. Intente más tarde.'; // mensaje genérico
});
Mejor aún: integra Monolog u otro logger y envía alertas en errores críticos.
4) Gestión insegura de sesiones (session fixation)
Por qué duele: si no renuevas el ID tras autenticación, un atacante que fije una sesión puede secuestrarla.
// MAL
// Inicio de sesión sin regenerar
session_start();
// ... verificar credenciales
$_SESSION['user_id'] = $id;
Solución: configura cookies seguras y regenera ID en login/logout.
// BIEN
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => 'tudominio.com',
'secure' => true, // HTTPS obligatorio
'httponly' => true, // evita acceso JS
'samesite' => 'Lax', // o 'Strict' según caso
]);
session_start();
// Tras autenticación correcta
session_regenerate_id(true); // destruye el viejo ID
$_SESSION['user_id'] = $id;
Adicional: invalida sesión en logout, limita intentos de login y considera usar tokens de renovación.
5) Manejo inseguro de uploads
Por qué duele: confiar en el nombre o en el MIME reportado por el navegador puede permitir subir archivos maliciosos (.php) que se ejecuten en el servidor.
// MAL
move_uploaded_file($_FILES['f']['tmp_name'], '/var/www/html/uploads/' . $_FILES['f']['name']);
Solución: valida tamaño, tipo real con finfo, guarda fuera del webroot o fuerza headers para descartar ejecución, y genera nombres únicos.
// BIEN
$max = 5 * 1024 * 1024; // 5 MB
if ($_FILES['f']['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload error');
}
if ($_FILES['f']['size'] > $max) {
throw new RuntimeException('Archivo demasiado grande');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['f']['tmp_name']);
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf'];
if (!isset($allowed[$mime])) {
throw new RuntimeException('Tipo de archivo no permitido');
}
$ext = $allowed[$mime];
$targetDir = '/var/www/private_uploads/'; // fuera del webroot
$filename = bin2hex(random_bytes(16)) . '.' . $ext;
if (!move_uploaded_file($_FILES['f']['tmp_name'], $targetDir . $filename)) {
throw new RuntimeException('No se pudo guardar el archivo');
}
Además: evita ejecutar archivos desde el directorio de uploads y aplica permisos restrictivos.
Buenas prácticas transversales (rápido)
- Usa PDO/ORMs con parametrización.
- Aplica password_hash y revisa rehashing.
- Controla errores según entorno y registra todo en logs centralizados.
- Configura cookies de sesión seguras y usa HTTPS obligatorio.
- Valida y trata todos los inputs del cliente como maliciosos por defecto.
Consejo avanzado: incorpora análisis estático (PHPStan, Psalm), pruebas de seguridad en CI (dependabot, composer audit), y revisiones periódicas de dependencias. Si ya aplicas lo anterior, el siguiente paso es realizar un pentest automatizado y probar remediaciones en un entorno staging.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación