Vulnerabilidades críticas en PHP y cómo solucionarlas
Esta guía repasa las vulnerabilidades más comunes en aplicaciones PHP modernas, muestra ejemplos vulnerables y proporciona correcciones concretas y recomendaciones operativas. Está pensada para desarrolladores que quieren mejorar la seguridad desde el código hasta la configuración y la infraestructura.
Índice rápido
- Inyección SQL
- XSS (Cross-Site Scripting)
- CSRF (Cross-Site Request Forgery)
- Subida de archivos insegura
- Inclusión remota/Local File Inclusion
- Ejecución de comandos (RCE / Command Injection)
- Fuga de sesiones y cookies inseguras
- Deserialización insegura
- Configuración de PHP insegura
1) Inyección SQL
Qué pasa: concatenar entradas del usuario en SQL permite que un atacante altere la consulta.
<?php
// VULNERABLE
$unsafe = $_GET['id'] ?? '';
$sql = "SELECT * FROM users WHERE id = $unsafe";
$result = $pdo->query($sql);
?>
Solución: siempre usar sentencias preparadas con parámetros (PDO o mysqli). Ventaja: evita la interpretación del input como SQL.
<?php
// SEGURO
$id = $_GET['id'] ?? '';
$stmt = $pdo->prepare('SELECT id, name, email FROM users WHERE id = :id');
$stmt->execute([':id' => $id]);
$user = $stmt->fetch();
?>
Por qué: el motor de la base de datos trata el parámetro como valor, no como parte de la consulta.
2) XSS (Cross-Site Scripting)
Qué pasa: imprimir contenido controlado por el usuario sin escape permite inyección de HTML/JS.
<?php
// VULNERABLE
echo '<div>Mensaje: ' . $_GET['msg'] . '</div>';
?>
Solución: escapar la salida según el contexto (HTML, atributo, JS). En HTML usar htmlspecialchars con el charset apropiado.
<?php
// SEGURO (salida en HTML)
$msg = $_GET['msg'] ?? '';
echo '<div>Mensaje: ' . htmlspecialchars($msg, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>';
?>
Por qué: se previene que etiquetas o scripts se interpreten en el navegador. Recuerda: validar en entrada y escapar en salida (output encoding).
3) CSRF
Qué pasa: acciones sensibles (cambiar correo, transferencias) pueden ser forzadas por un sitio malicioso si no se protege la petición.
<?php
// Generar token al iniciar formulario
session_start();
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['csrf_token'];
?>
<form method='post' action='/account/update'>
<input type='hidden' name='csrf_token' value='<?php echo $token; ?>' />
<!-- campos -->
</form>
<?php
// Verificar token en el handler
session_start();
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf_token'] ?? '')) {
http_response_code(400);
exit('Token inválido');
}
// procesar la acción
?>
Por qué: el token debe ser impredecible, único por sesión (o por formulario) y verificado con hash_equals para evitar timing attacks.
4) Subida de archivos
Riesgo: subir imágenes o archivos sin comprobar puede permitir scripts, overwrite y exfiltración.
Buenas prácticas:
- Almacenar uploads fuera del directorio público o servirlos mediante un controlador que valide permisos.
- Renombrar archivos con nombres únicos (hash + extensión permitida).
- Comprobar MIME con
finfo_filey, para imágenes, validar congetimagesize. - Limitar tamaño, desinfectar metadatos y establecer permisos restrictivos (600/640).
<?php
$allowedExt = ['jpg','jpeg','png','gif'];
$upload = $_FILES['file'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($upload['tmp_name']);
$ext = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExt) || !in_array($mime, ['image/jpeg','image/png','image/gif'])) {
http_response_code(400);
exit('Tipo de archivo no permitido');
}
// nombre seguro
$filename = bin2hex(random_bytes(16)) . '.' . $ext;
$destination = __DIR__ . '/../storage/uploads/' . $filename; // fuera de /public
move_uploaded_file($upload['tmp_name'], $destination);
?>
5) Inclusión remota / LFI / RFI
Evita construir rutas dinámicas a ficheros con input del usuario. Desactivar allow_url_include y validar rutas con whitelist o mapear claves a rutas concretas.
<?php
// VULNERABLE
$page = $_GET['page'];
include __DIR__ . '/pages/' . $page . '.php';
// SEGURO (mapa)
$map = ['home' => 'home.php', 'about' => 'about.php'];
$key = $_GET['page'] ?? 'home';
if (!isset($map[$key])) {
http_response_code(404);
exit('No encontrado');
}
include __DIR__ . '/pages/' . $map[$key];
?>
6) Ejecución de comandos (Command Injection)
No pases input directo a exec, system, passthru o backticks. Usa funciones nativas o escapes seguros.
<?php
// VULNERABLE
$user = $_GET['user'];
$output = shell_exec('ping -c 1 ' . $user);
// SEGURO: validar formato o usar escapeshellarg
$host = $_GET['host'];
if (!preg_match('/^[a-z0-9\.-]+$/i', $host)) {
exit('Host inválido');
}
$escaped = escapeshellarg($host);
$output = shell_exec('ping -c 1 ' . $escaped);
?>
7) Sesiones y cookies inseguras
Asegura cookies y gestión de sesiones:
- Usa
session_set_cookie_paramspara marcarhttponly,secureysamesite. - Regenera el ID de sesión al autenticar (
session_regenerate_id(true)). - Mantén el almacenamiento de sesiones en un backend seguro (Redis, memcached con ACLs) y establece lifetime razonable.
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => 'example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
?>
8) Deserialización insegura
Evita unserialize en datos no confiables: puede crear objetos con comportamiento no esperado (gadgets) que permitan RCE.
Si debes deserializar, usa JSON (json_encode/json_decode) o deserialización segura con allowed_classes en unserialize:
<?php
// Evitar
$data = unserialize($untrusted);
// Mejor
$data = json_decode($untrusted, true);
// Si usas unserialize por necesidad
$allowed = ['MyDTO'];
$data = unserialize($untrusted, ['allowed_classes' => $allowed]);
?>
9) Configuración de PHP y prácticas operativas
- php.ini:
display_errors = Off,log_errors = On,allow_url_include = Off. - Deshabilitar funciones peligrosas si no se necesitan:
disable_functions = exec,passthru,shell_exec,system,proc_open,popen. - Usar HTTPS con HSTS; servir cookies secure.
- Limitar permisos de archivos y usuarios del proceso PHP-FPM.
- Actualiza PHP y extensiones; usa versiones LTS soportadas.
Herramientas y prácticas recomendadas
- Dependabot / Composer audit / Security advisories para dependencias.
- Escaneo dinámico: OWASP ZAP, Burp Suite.
- Escaneo estático: SonarQube, Psalm, PHPStan con reglas de seguridad.
- Pruebas de penetración periódicas y revisión de código con checklist de seguridad.
Ejemplo de estructura de proyecto segura
/project-root
├─ /public # Document root: sólo index.php, assets
│ └─ index.php
├─ /src # Lógica de aplicación
├─ /config # Config y secrets (fuera de VCS, usar env/secret manager)
├─ /storage
│ └─ /uploads # uploads fuera de public
├─ /logs
└─ composer.json
Checklist rápido para desplegar una app PHP segura
- Usar PDO con parámetros en todas las consultas.
- Escapar salida con
htmlspecialcharsy considerar escaping específico por contexto. - Proteger formularios con tokens CSRF.
- Validar y sanear uploads y almacenarlos fuera del webroot.
- Configurar cookies y sesiones con
secure,httponly,samesite. - Desactivar funciones PHP peligrosas y
display_errorsen producción. - Usar
password_hashypassword_verifypara contraseñas. - Ejecutar scans automáticos en CI y aplicar parches regularmente.
Recursos y referencias
- OWASP Top 10
- PHP: The Right Way (sección security)
- Documentación de PDO, password_hash, finfo
Advertencia: la seguridad es un proceso continuo. Empieza por las correcciones críticas (inyección, XSS, CSRF) y avanza hacia controles operativos y detección.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación