5 errores críticos que todos cometen con PHP (y cómo solucionarlos)
PHP es flexible y rápido para prototipar, pero esa flexibilidad trae trampas. Aquí tienes cinco errores frecuentes—con ejemplos reales, por qué son peligrosos y cómo arreglarlos correctamente.
1) Concatenar consultas SQL en lugar de usar consultas preparadas (inyección SQL)
Por qué importa: la inyección SQL sigue siendo una de las vulnerabilidades más explotadas. Usar concatenación permite que datos del usuario modifiquen la consulta.
Mal (vulnerable):
<?php
// Usuario controla 'id'
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
$rows = $pdo->query($sql)->fetchAll();
?>
Bien (PDO con prepared statements):
<?php
$id = $_GET['id'];
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute([':id' => $id]);
$rows = $stmt->fetchAll();
?>
Por qué funciona: las consultas preparadas separan código y datos; el driver se encarga de escapar y bindear valores correctamente.
2) Almacenar contraseñas en texto plano o usar hashing débil
Por qué importa: si tu base de datos se filtra, las contraseñas deben ser inútiles para el atacante.
Mal: guardar la contraseña tal cual o usar md5/sha1.
<?php
// NO
$stored = $_POST['password'];
// o
$hash = md5($_POST['password']);
?>
Bien (password_hash / password_verify):
<?php
// Al registrar
$hash = password_hash($_POST['password'], PASSWORD_DEFAULT);
// Al autenticar
if (password_verify($_POST['password'], $hash_from_db)) {
// contraseña correcta
}
?>
Por qué funciona: password_hash aplica un algoritmo fuerte y sal único; password_verify maneja la comparación segura y soporta rehash cuando cambias el algoritmo.
3) No escapar salida (XSS)
Por qué importa: mostrar datos del usuario sin escapar permite inyectar scripts que roban cookies o realizan acciones en su nombre.
Mal:
<?php
echo '<div>Usuario: ' . $user['name'] . '</div>';
?>
Bien (escape contextual con htmlspecialchars):
<?php
echo '<div>Usuario: ' . htmlspecialchars($user['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>';
?>
Por qué funciona: escapar convierte < > " ' en entidades seguras. Usa escaping contextual (HTML, attribute, JS, URL) según dónde insertes el dato.
4) Manejo inseguro de sesiones (session fixation / IDs predecibles)
Por qué importa: sesiones mal protegidas permiten secuestrar cuentas. Problemas comunes: no regenerar ID al login, no limitar cookies, no usar HTTPS/secure flags.
Mejor configuración y prácticas:
<?php
// Al inicio de la app (antes de session_start)
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => 'example.com',
'secure' => true, // solo HTTPS
'httponly' => true, // no accesible por JS
'samesite' => 'Lax',
]);
session_start();
// Cuando el usuario hace login:
session_regenerate_id(true); // evita fixation
$_SESSION['user_id'] = $userId;
?>
Por qué funciona: secure/httponly/samesite ayudan a proteger la cookie; regenerar ID evita que un atacante reutilice el ID anterior.
5) Validación insuficiente en uploads y deserialización insegura
Por qué importa: subir archivos sin validar puede llevar a RCE (ejecutar scripts), y unserialize() con datos no confiables permite object injection.
Mal:
<?php
move_uploaded_file($_FILES['f']['tmp_name'], '/var/www/html/uploads/' . $_FILES['f']['name']);
// o
$data = unserialize($_POST['data']);
?>
Bien:
<?php
// Validar upload
$allowed = ['image/png', 'image/jpeg'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$type = $finfo->file($_FILES['f']['tmp_name']);
if (!in_array($type, $allowed, true)) {
throw new RuntimeException('Tipo de archivo no permitido');
}
$ext = $type === 'image/png' ? '.png' : '.jpg';
$target = sprintf('/var/www/uploads/%s%s', bin2hex(random_bytes(16)), $ext);
move_uploaded_file($_FILES['f']['tmp_name'], $target);
// Evitar unserialize: usar json
$data = json_decode($_POST['data'], true);
?>
Por qué funciona: comprobar MIME y generar nombres aleatorios evita sobreescrituras y ejecución. Usar JSON evita la ejecución de objetos mágicos en clase deserializada.
Buenas prácticas transversales
- Configura display_errors = off en producción y registra errores en ficheros seguros.
- Principio de mínimo privilegio en la BD: cuenta DB por app con permisos limitados.
- Usa frameworks modernos que te dan protecciones por defecto (CSRF tokens, escaping, routing).
- Revisa dependencias: Composer audit / SensioLabs Security Checker (o herramientas actuales).
Implementar estas correcciones protegerá tu aplicación frente a los vectores más comunes. Siguiente paso: prueba cada cambio con casos adversos (pentest básico) y añade CI que ejecute comprobaciones de seguridad automatizadas para que estas buenas prácticas no se pierdan con el tiempo.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación