Guía completa de seguridad en PHP para desarrolladores backend
La seguridad en aplicaciones PHP no es opcional: es una obligación. En esta guía práctica verás las amenazas más comunes, ejemplos de código seguros, estructura recomendada del proyecto y por qué cada medida reduce riesgo real. Voy directo al grano, con ejemplos listos para integrar.
1. Principios básicos y configuración segura
- Siempre usa HTTPS y HSTS. HTTP es inseguro para transporte de credenciales y cookies.
- Desactiva la salida de errores en producción: display_errors = Off en php.ini y controla logs.
- Usa versiones de PHP soportadas y aplica parches. Mantén dependencias actualizadas con composer.
; php.ini (relevante para producción)
display_errors = Off
error_reporting = E_ALL
log_errors = On
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
2. Estructura de proyecto recomendada
mi-app-php/
composer.json
public/ # Document root
index.php
src/
Auth/
Password.php
Security/
Session.php
Csrf.php
Controllers/
config/
config.php # variables de entorno via env
storage/
uploads/ # fuera del webroot o con reglas estrictas
logs/
tests/
Separar concerns facilita aplicar políticas de seguridad coherentes (p. ej. todas las sesiones desde Security/Session).
3. Validación y saneamiento de entrada
Valida por tipo y usa listas blancas. Nunca confíes en el cliente.
// Ejemplo de validación mínima
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 120]]);
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($age === false || $email === false) {
http_response_code(400);
echo 'Entrada inválida';
exit;
}
Por qué: filtrar evita tipos inesperados y reduce superficie de ataque (inyecciones, lógica rota).
4. Prevención de SQL Injection: PDO con prepared statements
// db.php (ejemplo simple)
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = :email');
$stmt->execute([':email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
Por qué: los prepared statements separan datos de la lógica SQL y eliminan la posibilidad de manipular la consulta.
5. Autenticación y gestión de contraseñas
Usa password_hash() y password_verify(). No inventes algoritmos propios. Considera sal + pepper si quieres añadir defensa adicional.
// Password.php
function createHash(string $password): string {
// PASSWORD_ARGON2ID si está disponible, si no PASSWORD_DEFAULT
return password_hash($password, PASSWORD_DEFAULT);
}
function verifyPassword(string $password, string $hash): bool {
return password_verify($password, $hash);
}
Por qué: password_hash maneja sal y parámetros de coste. Cambia el algoritmo con el tiempo y re-hashear si necesario.
6. Sesiones seguras
// Security/Session.php
function startSecureSession(array $options = []) {
$defaults = [
'cookie_httponly' => true,
'cookie_secure' => true, // requiere HTTPS
'use_strict_mode' => true,
'cookie_samesite' => 'Lax'
];
ini_set('session.use_only_cookies', '1');
foreach ($defaults as $k => $v) ini_set('session.' . $k, $v ? '1' : '0');
session_start();
if (empty($_SESSION['initiated'])) {
session_regenerate_id(true);
$_SESSION['initiated'] = true;
}
}
Por qué: regenerar id previene session fixation; SameSite y secure reducen riesgo de CSRF y robo de cookie.
7. CSRF (Cross-Site Request Forgery)
Implementa tokens sincronizados (Synchronizer Token Pattern) o doble cookie. Ejemplo sencillo:
// Security/Csrf.php
function csrfToken(): string {
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf'];
}
function validateCsrf(string $token): bool {
return hash_equals($_SESSION['csrf'] ?? '', $token);
}
// Uso en formulario
$token = csrfToken();
// <input type='hidden' name='csrf' value='<?= $token ?>'>
Por qué: los tokens imparables por un atacante externo evitan que formularios maliciosos se sometan en nombre del usuario.
8. Cross-Site Scripting (XSS) y Content Security Policy (CSP)
Escapa toda salida dinámica según el contexto. Para HTML use htmlspecialchars. Para atributos y JS, aplicar escapes adecuados.
function e(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
// En plantilla: <div><?= e($user['name']) ?></div>
Ejemplo de header CSP:
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-...'; object-src 'none';");
Por qué: CSP reduce el impacto de inyecciones de scripts y obliga a cargar recursos sólo desde orígenes confiables.
9. Manejo seguro de uploads
- Guarda archivos fuera del webroot o con nombres aleatorios.
- Valida tipo MIME y extensión, pero confía más en inspección del contenido (getimagesize para imágenes).
- Limita tamaño y establece permisos de fichero mínimos.
// upload simple
if (isset($_FILES['file'])) {
$f = $_FILES['file'];
if ($f['error'] !== UPLOAD_ERR_OK) throw new RuntimeException('Upload error');
if ($f['size'] > 2 * 1024 * 1024) throw new RuntimeException('Archivo demasiado grande');
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $f['tmp_name']);
if (!in_array($mime, ['image/jpeg', 'image/png'])) throw new RuntimeException('Tipo no permitido');
$target = __DIR__ . '/../storage/uploads/' . bin2hex(random_bytes(16));
move_uploaded_file($f['tmp_name'], $target);
}
10. Cabeceras HTTP útiles
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: no-referrer-when-downgrade');
Por qué: estas cabeceras cierran vectores comunes (clickjacking, content type sniffing, etc.).
11. Gestión de secretos y configuración
No guardes credenciales en el repo. Usa variables de entorno y control de accesos. Ejemplo con phpdotenv:
composer require vlucas/phpdotenv
Y en config/config.php lee $_ENV o getenv. Protege copias de seguridad y backups.
12. Registro, monitoreo y respuesta
- Registra eventos de seguridad (login fallidos, cambios de contraseña, uploads) con contexto mínimo.
- Instrumenta alertas y revisa logs con regularidad.
13. Herramientas y pruebas
- Static analysis: PHPStan, Psalm
- Dependencias: composer audit o SensioLabs Security Checker (según disponibilidad)
- Escaneo dinámico: OWASP ZAP, Burp Suite
- Fuzzing y tests automatizados de endpoints
14. Errores comunes y cómo evitarlos
- Exponer detalles de error en producción — desactivar display_errors.
- Construir consultas con concatenación de strings — usar prepared statements.
- Almacenar archivos subidos en webroot sin validación — mover fuera del webroot.
- Confiar en JavaScript para validaciones críticas — validar en servidor también.
15. Checklist rápido para deploy seguro
- TLS configurado y forzado (HSTS)
- php.ini con display_errors=Off y logs habilitados
- Cookies con Secure, HttpOnly y SameSite
- Prepared statements y escapes por contexto
- Cabeceras CSP, X-Frame-Options, X-Content-Type-Options
- Escaneo de dependencias y análisis estático pasados
La seguridad es un proceso continuo. Empieza por las defensas básicas (HTTPS, sesiones seguras, prepared statements) y añade capas: validación estricta, CSP, escaneo regular y respuesta a incidentes. Un paso práctico ahora: instrumenta logs de autenticación y activa alertas para múltiples intentos fallidos. Si quieres, puedo generar el esqueleto del proyecto con los archivos de ejemplo listos para arrancar y tests básicos para validar estas políticas.
Consejo avanzado: considera mover funciones críticas (p. ej. validación, encriptado de secretos) a procesos o servicios aislados y delega autenticación a proveedores externos confiables cuando la seguridad y cumplimiento sean críticos.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación