Vulnerabilidades críticas en PHP y cómo solucionarlas (guía práctica)
PHP domina muchas aplicaciones web; su rendimiento y velocidad de desarrollo explican su uso masivo. Pero también es un objetivo frecuente para atacantes por malas prácticas y configuraciones inseguras. Aquí tienes una guía práctica con ejemplos de código, configuraciones y contramedidas concretas.
Mapa rápido
- Inyección SQL
- XSS (Cross-Site Scripting)
- CSRF (Cross-Site Request Forgery)
- Subida de archivos insegura
- Deserialización insegura
- Gestión de sesiones
- Funciones peligrosas y configuración de php.ini
- Buenas prácticas y herramientas
1) Inyección SQL — la base
Problema: concatenar entrada del usuario en queries. Solución: sentencias preparadas (PDO) y validación.
<?php
// BAD: vulnerable
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
// GOOD: prepared statements
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $_GET['id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
?>
Tips: usar tipos estrictos cuando sea posible, validar y cast al tipo esperado (por ejemplo (int)$_GET['id']). Para consultas complejas usar ORM o query builder que abstraigan parámetros.
2) XSS — salida insegura
Problema: imprimir HTML con contenido controlado por usuario. Solución: escapar salida según contexto y aplicar Content Security Policy.
<?php
// Escapar para HTML
echo htmlspecialchars($user['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
// Si necesitas insertar en atributo JS o HTML, usar context-appropriate escaping
?>
Cabeceras recomendadas:
header('Content-Security-Policy: default-src \'self\'; script-src \'self\'');
header('X-Content-Type-Options: nosniff');
3) CSRF — prevenir acciones forzadas
Problema: formularios o endpoints que aceptan acciones con privilegios sin verificar la intención del usuario. Solución: tokens CSRF únicos por sesión/acción.
<?php
// Generar token (en inicio de sesión o creación del formulario)
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// Incluir en formulario
?>
<form method='post' action='/transfer'>
<input type='hidden' name='csrf_token' value='<?= $_SESSION['csrf_token'] ?>'>
</form>
<?php
// Validar token
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf_token'] ?? '')) {
http_response_code(403);
exit('Invalid CSRF token');
}
?>
Si usas frameworks (Laravel, Symfony, etc.) habilita su protección CSRF incorporada.
4) Subida de archivos — control estricto
Problema: aceptar archivos sin validar puede llevar a ejecución remota, traversals, o sobrescritura. Solución: whitelist de extensiones, validación MIME con finfo, renombrado aleatorio, almacenamiento fuera del webroot, uso de move_uploaded_file.
<?php
$allowed_ext = ['jpg','png','pdf'];
$upload = $_FILES['file'] ?? null;
if ($upload && $upload['error'] === UPLOAD_ERR_OK) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($upload['tmp_name']);
// Validar mime (ejemplo simple)
$ext = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_ext, true)) {
exit('Extensión no permitida');
}
// Nombre seguro
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$target = __DIR__ . '/uploads/' . $newName; // idealmente fuera del webroot
if (!move_uploaded_file($upload['tmp_name'], $target)) {
exit('Error al mover archivo');
}
}
?>
Además: bloquear ejecución en carpeta de uploads mediante .htaccess o configuración del servidor.
5) Deserialización insegura
Problema: unserialize() en datos controlados por usuario permite invocar magia de objetos con side effects. Solución: evitar unserialize; usar json_decode; si imprescindible, restringir allowed_classes.
<?php
// Malo:
$data = unserialize($_POST['data']);
// Mejor:
$data = json_decode($_POST['data'], true);
// Si usas unserialize y confías parcialmente:
$allowed = ['MySafeClass'];
$data = unserialize($raw, ['allowed_classes' => $allowed]);
?>
Evita almacenar objetos serializados que puedan rehidratarse con control del atacante.
6) Gestión de contraseñas y autenticación
Usar hashing moderno y salting incorporado: password_hash / password_verify.
<?php
$hash = password_hash($password, PASSWORD_DEFAULT);
if (password_verify($password, $hash)) {
// OK
}
?>
No uses MD5 o SHA1 para contraseñas. Implementa políticas de bloqueo por intentos y 2FA si es posible.
7) Sesiones seguras
Configuraciones clave y regeneración de ID.
<?php
ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // requiere HTTPS
ini_set('session.cookie_samesite', 'Lax');
session_start();
// Regenerar ID después de login
session_regenerate_id(true);
?>
Almacena sesiones en un storage seguro (redis con autenticación o directorio fuera del webroot con permisos correctos).
8) Funciones peligrosas y php.ini
Deshabilitar funciones que no necesitas:
; php.ini
expose_php = Off
display_errors = Off
log_errors = On
allow_url_fopen = Off
allow_url_include = Off
; Deshabilitar funciones peligrosas
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
open_basedir = "/var/www/yourapp/:/tmp/"
Esto reduce la superficie de ataque y evita revelación de versiones.
9) Configuración segura del servidor y despliegue
- Usa HTTPS siempre (HSTS)
- Setea cabeceras de seguridad: X-Frame-Options, X-XSS-Protection (aunque CSP es la mejor defensa), Referrer-Policy
- Minimiza permisos de usuario del proceso PHP-FPM
- Mantén dependencias actualizadas y ejecuta auditorías de Composer: composer audit
10) Registro, monitoreo y detección
Registra eventos de seguridad (inicios de sesión fallidos, errores fatales), integra con SIEM, y establece alertas. No registres contraseñas ni tokens en texto plano.
11) Herramientas y prácticas recomendadas
- Static analysis: PHPStan, Psalm
- Dependency scanning: Roave Security Advisories + composer audit
- Fuzzing y tests de integración para endpoints críticos
- CI/CD: ejecutar linters, análisis estático y tests antes de deploy
12) Checklist rápido (prioriza)
- Deshabilitar display_errors en producción
- Usar prepared statements para todas las consultas
- Escapar toda salida por contexto
- Implementar tokens CSRF para formularios que modifican estado
- Validar y sanear uploads + almacenamiento fuera del webroot
- Eliminar uso de unserialize en entradas no confiables
- Usar password_hash y políticas de autenticación robustas
- Configurar cookies de sesión seguras y regenerar ID al login
- Auditar dependencias y aplicar parches
13) Ejemplo de configuración segura mínima (snippet)
; php.ini (fragmento)
expose_php = Off
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
allow_url_fopen = Off
allow_url_include = Off
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
session.use_strict_mode = 1
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"
open_basedir = "/var/www/yourapp/:/tmp/"
14) Respuesta ante incidentes
Si detectas una intrusión: 1) aisla el servicio, 2) toma copias forenses, 3) rota credenciales y claves, 4) revisa logs para vector de entrada, 5) aplica parches y mitigaciones, 6) comunica a usuarios si es necesario.
Seguridad en PHP no es solo parchear fallos individuales, es establecer una defensa en profundidad: código seguro, configuración correcta, monitoreo y procesos de respuesta. Empieza por lo crítico (inyección, XSS, autenticación) y automatiza el resto en CI/CD.
Siguiente paso: habilita análisis estático (PHPStan o Psalm) en tu pipeline y realiza un escaneo de dependencias; si quieres, puedo mostrarte cómo integrar PHPStan y composer audit en GitHub Actions con ejemplos prácticos.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación