Llevo desarrollando desde 2010 y he visto de todo: desde intentos de inyección SQL tan básicos que dan risa, hasta ataques sofisticados que me hicieron replantear toda mi arquitectura. Hoy quiero compartir lo que he aprendido en estos 14 años, especialmente ahora que en Ecuador la Ley Orgánica de Protección de Datos Personales nos obliga a tomarnos la seguridad aún más en serio.
No voy a mentir: en mis primeros años cometí errores. Pero la diferencia entre un desarrollador junior y uno senior no es no cometer errores, es aprender rápido de ellos. Y créeme, después de ver cómo intentaban entrar a una base de datos con información médica que manejaba en 2018, la paranoia se volvió mi mejor amiga.
Inyección SQL: El clásico que nunca muere
Incluso en 2025, sigo viendo código vulnerable. Hace unos meses, auditando el código de un cliente, encontré la típica bomba de tiempo:
// NUNCA hagas esto
$id = $_GET['id'];
$query = "SELECT * FROM clientes WHERE id = $id";
El problema no es solo técnico. Con la Ley de Protección de Datos en Ecuador, una filtración así puede costarte multas de hasta 1% de tus ingresos anuales. La solución es simple pero muchos la ignoran: usa PDO con consultas preparadas.
// La forma correcta
$stmt = $pdo->prepare("SELECT * FROM clientes WHERE id = :id");
$stmt->execute(['id' => $id]);
¿Por qué funciona? Los placeholders separan completamente los datos del SQL. No importa qué inyecten, nunca será interpretado como código.
XSS: El dolor de cabeza silencioso
En 2019, un sistema de gestión hospitalaria que desarrollé fue "decorado" con mensajes ofensivos. El atacante no robó datos, pero ejecutó JavaScript que mostraba alertas a todos los usuarios. El vector? Un campo de "observaciones médicas" sin sanitizar.
La lección fue clara: SIEMPRE escapa el output. Creé una función simple que uso en todos mis proyectos:
function e($string) {
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
// En las vistas
echo "Bienvenido, " . e($usuario['nombre']);
Simple pero efectiva. Para contenido más complejo, uso HTMLPurifier con una lista blanca estricta de tags permitidos.
CSRF: El ataque ninja
En 2021, un cliente grande (una clínica) casi cancela el contrato porque alguien logró hacer que los usuarios cambiaran sus contraseñas sin darse cuenta. El atacante enviaba emails con imágenes que, al cargar, ejecutaban cambios en el sistema.
La solución: tokens CSRF en todos los formularios. No es complicado:
// Generar token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// En el form
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';
// Al procesar
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
die('Token inválido');
}
Contraseñas: La evolución necesaria
Mi hall of shame personal con contraseñas:
- 2010-2012: MD5 (sé lo que piensas)
- 2013-2015: SHA256 con salt
- 2016-2018: bcrypt
- 2019-presente: password_hash() de PHP
La pregunta más común: "¿Cuál es la forma correcta de guardar contraseñas?" La respuesta en 2025:
// Al registrar
$hash = password_hash($password, PASSWORD_DEFAULT);
// Al verificar
if (password_verify($password, $hash)) {
// Login exitoso
// Bonus: actualizar hash si es necesario
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
// Actualizar en BD
}
}
PASSWORD_DEFAULT actualmente usa bcrypt, pero PHP lo actualizará automáticamente a algo mejor en el futuro. Es a prueba de futuro.
Validación: Tu primera línea de defensa
Con los años aprendí que validar bien ahorra el 80% de los problemas. Mi approach actual es simple pero efectivo:
// Validar email
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Email inválido";
}
// Validar cédula ecuatoriana (lo necesito seguido)
if (!preg_match('/^[0-9]{10}$/', $cedula)) {
$errors[] = "Cédula inválida";
}
// Sanitizar siempre
$nombre = filter_var($nombre, FILTER_SANITIZE_STRING);
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
Cumplimiento LOPDP Ecuador
Desde mayo 2021, la Ley Orgánica de Protección de Datos Personales cambió el juego. Ya no es solo buena práctica, es obligatorio. Los puntos clave que implemento:
1. Consentimiento explícito
Guardo registro de cuándo y para qué el usuario dio permiso de usar sus datos. Un simple checkbox no es suficiente, necesitas evidencia.
2. Derecho al olvido
Los usuarios pueden pedir que borres sus datos. En lugar de DELETE (que rompe relaciones), uso anonimización:
// En vez de borrar, anonimizar
UPDATE usuarios SET
email = CONCAT('deleted_', id, '@local'),
nombre = 'Usuario Eliminado',
cedula = NULL
WHERE id = ?
3. Notificación de brechas
Tienes 72 horas para notificar a la autoridad si hay una filtración. Por eso mis logs son exhaustivos pero sin datos sensibles.
4. Logs de acceso
Registro quién accede a qué datos y cuándo. Es tedioso pero necesario:
// Cada vez que se accede a datos sensibles
$log = [
'user_id' => $_SESSION['user_id'],
'action' => 'view_patient_data',
'timestamp' => date('Y-m-d H:i:s'),
'ip' => $_SERVER['REMOTE_ADDR']
];
// Guardar en tabla de auditoría
Mis herramientas esenciales de seguridad
Después de años, estas son las herramientas que no pueden faltar:
- Headers de seguridad: X-Frame-Options, Content-Security-Policy, etc.
- Rate limiting: Para evitar fuerza bruta
- Logs detallados: Pero sin información sensible
- HTTPS siempre: Let's Encrypt es gratis, no hay excusa
- Actualizaciones: PHP 8.x tiene mejoras de seguridad importantes
El checklist que uso antes de cada deploy
Simple pero efectivo:
- ¿Están todas las queries usando prepared statements?
- ¿Todo output está escapado?
- ¿Los formularios tienen tokens CSRF?
- ¿Las contraseñas usan password_hash?
- ¿Los errores están desactivados en producción?
- ¿Hay logs de acceso a datos sensibles?
- ¿Cumple con LOPDP? (consentimiento, derecho al olvido, etc.)
Reflexiones finales
La seguridad no es un destino, es un viaje constante. Cada año aparecen nuevas vulnerabilidades y nuevas regulaciones. En Ecuador, con la LOPDP, ya no es opcional ser paranoico con la seguridad.
Mi consejo después de 14 años: no esperes a que te hackeen para tomarte la seguridad en serio. Es más barato prevenir que lamentar, especialmente cuando las multas pueden ser del 1% de tus ingresos anuales.
Y recuerda: la seguridad no es responsabilidad de una persona, es responsabilidad de todo el equipo. Cada línea de código puede ser una puerta de entrada o una barrera.
¿Paranoia? Tal vez. ¿Necesaria? Absolutamente. Especialmente cuando manejas datos de personas reales que confían en ti.
Stay safe, code safer. 🔒
Comentarios 1
Comparte tu opinión
Roberto Rodríguez