Vulnerabilidades críticas en PHP y cómo solucionarlas
PHP sigue siendo una pieza clave en la web. Pero también es un objetivo frecuente para atacantes. Aquí tienes un compendio práctico: vulnerabilidad, ejemplo vulnerable, por qué ocurre y cómo corregirla con código real y recomendaciones de configuración.
1. Inyección SQL
Qué pasa: input sin sanear se concatena en consultas SQL, permitiendo manipulación del query.
Ejemplo vulnerable:
<?php
// vulnerable.php (NO usar)
$username = $_POST['username'];
$query = "SELECT * FROM users WHERE username = '" . $username . "'";
$result = $pdo->query($query);
?>
Por qué: concatenación directa y ausencia de parámetros. Un atacante puede enviar "' OR '1'='1".
Solución (PDO con prepared statements):
<?php
// safe-sql.php
$stmt = $pdo->prepare('SELECT id, username FROM users WHERE username = :username');
$stmt->execute(['username' => $_POST['username'] ?? '']);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
?>
Por qué funciona: parámetros ligan valores sin alterar la estructura del SQL.
2. Cross-Site Scripting (XSS)
Qué pasa: salida de datos controlados por el usuario sin escapar en HTML.
Ejemplo vulnerable:
<?php
// profile.php (vulnerable)
echo "<h1>" . $user['display_name'] . "</h1>";
?>
Solución: escapar o codificar la salida según el contexto. Para HTML: htmlspecialchars.
<?php
// profile-safe.php
function e($s) { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
echo "<h1>" . e($user['display_name']) . "</h1>";
?>
Consejo: para atributos, JS, URLs y CSS usa el método de escape apropiado o una librería de templates que lo haga por ti (Twig, Plates, Blade).
3. CSRF (Cross-Site Request Forgery)
Qué pasa: peticiones que tienen privilegios del usuario autenticado pueden ser forzadas desde otra web.
Ejemplo vulnerable:
<form method="post" action="/transfer.php">
<input name="amount">
<button>Send</button>
</form>
Solución: tokens CSRF por sesión y verificación en POST/acciones mutativas.
<?php
// csrf.php - helpers
session_start();
if (!isset($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
function csrf_input() {
return '';
}
// On form processing
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
http_response_code(403);
exit('Invalid CSRF token');
}
?>
Nota: usa SameSite cookies y verifica Origin/Referer cuando sea posible.
4. Subida de archivos insegura
Qué pasa: aceptar y mover archivos sin validar tipo, extensión, tamaño o ruta puede permitir ejecución remota.
Ejemplo vulnerable:
<?php
// upload.php (vulnerable)
move_uploaded_file($_FILES['file']['tmp_name'], '/var/www/html/uploads/' . $_FILES['file']['name']);
?>
Solución: validar MIME real, renombrar archivos, restringir rutas y ubicaciones fuera del webroot, establecer permisos mínimos.
<?php
// upload-safe.php
$allowed = ['image/png','image/jpeg'];
$f = $_FILES['file'] ?? null;
if (!$f || $f['error'] !== UPLOAD_ERR_OK) exit('Upload error');
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $f['tmp_name']);
if (!in_array($type, $allowed, true)) exit('Invalid file type');
$ext = $type === 'image/png' ? '.png' : '.jpg';
$destName = bin2hex(random_bytes(16)) . $ext;
$destPath = '/var/uploads/' . $destName; // fuera del webroot
move_uploaded_file($f['tmp_name'], $destPath);
?>
Adicional: escanea los ficheros con antivirus y establece permisos 0600/0640.
5. Remote/Local File Inclusion (RFI/LFI)
Qué pasa: incluir archivos basados en input del usuario puede ejecutar código arbitrario o leer archivos del sistema.
Vulnerable:
<?php
// index.php (vulnerable)
$page = $_GET['page'];
include 'pages/' . $page . '.php';
?>
Arreglo: whitelist de páginas y/o mapear rutas explícitamente.
<?php
$allowed = ['home' => 'pages/home.php', 'about' => 'pages/about.php'];
$page = $_GET['page'] ?? 'home';
if (!isset($allowed[$page])) { http_response_code(404); exit('Not found'); }
include $allowed[$page];
?>
6. Command Injection
Qué pasa: pasar input directamente a funciones como exec(), system(), shell_exec() permite ejecución de comandos.
Vulnerable:
<?php
$ip = $_GET['ip'];
$output = shell_exec('ping -c 1 ' . $ip);
echo <pre> . $output . </pre>;
?>
Solución: evita invocar shell; usa librerías nativas. Si debes, valida estrictamente y usa escapeshellarg.
<?php
$ip = $_GET['ip'];
if (!filter_var($ip, FILTER_VALIDATE_IP)) { exit('Invalid IP'); }
$output = shell_exec('ping -c 1 ' . escapeshellarg($ip));
?>
7. Gestión de sesiones insegura
Problemas comunes: session fixation, cookies sin flags, sesión no regenerada al login.
Buenas prácticas:
- session_start(); luego session_regenerate_id(true) tras autenticación.
- Configurar cookies: session.cookie_httponly = On, session.cookie_secure = On (HTTPS), session.use_strict_mode = 1
- Limitar duración y destruir sesión en logout.
<?php
// after successful login
session_regenerate_id(true);
$_SESSION['user_id'] = $userId;
?>
8. Insecure deserialization
Qué pasa: unserialize() puede instanciar objetos con efectos secundarios (magic methods) o ejecutar __wakeup/__destruct.
Evitar usar unserialize() en datos no confiables. Usa json_encode/json_decode en su lugar o whitelist de clases.
// Evitar
$data = unserialize($_POST['data']);
// Preferir
$data = json_decode($_POST['data'], true);
9. Fugas de información y manejo de errores
Qué pasa: mostrar errores con stack traces o detalles de DB en producción revela vectores de ataque.
Configurar en production php.ini:
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
expose_php = Off
En tu app, captura excepciones y muestra mensajes genéricos al usuario mientras registras detalles internamente.
10. Configuración segura de PHP
- disable_functions = exec,passthru,shell_exec,system,popen,proc_open (bloquea funciones peligrosas)
- allow_url_fopen = Off y allow_url_include = Off (previene inclusiones remotas)
- open_basedir = /var/www/html/:/tmp/ (limita accesos de filesystem)
- session.cookie_httponly = 1, session.cookie_secure = 1, session.use_strict_mode = 1
Herramientas y flujo de trabajo
Integra estas prácticas en CI/CD:
- Static analysis (PHPStan, Psalm)
- SAST/DAST (SonarQube, OWASP ZAP)
- Dependency checks (Composer audit, SensioLabs Security Checker)
- Pen testing y fuzzing regulares
Ejemplo: estructura mínima segura para un endpoint
Carpeta propuesta:
project/
public/index.php
src/
App.php
Controllers/
Middleware/
vendor/
logs/
.env
public/index.php (abridor seguro):
<?php
require __DIR__ . '/../vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->load();
// Force HTTPS (reverse proxy aware)
if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off') {
// redirigir o aplicar políticas
}
// Dispatcher simple: valida rutas, usa middleware para auth, CSRF y sanitización
?>
Checklist rápido para revisar una app PHP
- ¿Todas las queries usan prepared statements?
- ¿Salida HTML escapada por defecto?
- ¿Tokens CSRF en formularios mutativos?
- ¿Subidas fuera del webroot y validadas?
- ¿Funciones shell deshabilitadas si no son necesarias?
- ¿Errores ocultos en producción y logs seguros?
- ¿Sesiones con flags secure/httponly y regeneración de id?
Implementar estas medidas reduce significativamente el riesgo. Un paso avanzado: añade pruebas de seguridad automatizadas en tu CI (p. ej. OWASP ZAP en GitHub Actions) y reglas de seguridad en el código (PHPStan/Psalm con niveles altos).
Advertencia final: incluso con configuraciones seguras, las dependencias y el despliegue (exposición de puertos, permisos de filesystem, backups inseguros) son vectores críticos — revisa también la superficie de red y la pipeline de despliegue como siguiente paso.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación