Volver al Blog

Seguridad en PHP: Lecciones aprendidas después de 14 años esquivando hackers

Seguridad en PHP: Lecciones aprendidas después de 14 años esquivando hackers

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:

  1. ¿Están todas las queries usando prepared statements?
  2. ¿Todo output está escapado?
  3. ¿Los formularios tienen tokens CSRF?
  4. ¿Las contraseñas usan password_hash?
  5. ¿Los errores están desactivados en producción?
  6. ¿Hay logs de acceso a datos sensibles?
  7. ¿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. 🔒

C

Sobre Carlos Donoso

Full Stack Developer y AI Engineer apasionado por crear soluciones innovadoras. Me especializo en desarrollo web moderno, inteligencia artificial y automatización. Comparto conocimiento para ayudar a otros developers a crecer en su carrera.

Comentarios 1

Comparte tu opinión

0/1000 caracteres
Avatar de Roberto Rodríguez

Roberto Rodríguez

23/01/2025
Muy buen artículo, Carlos. La seguridad es un tema crucial y más ahora con la ley de protección de datos. Me pasó algo similar en 2019, cuando casi me hackean una app de gestión médica. Desde entonces, implemento medidas más robustas. La paranoia también es mi amiga!
Muy buen artículo, Carlos. La seguridad es un tema crucial y más ahora con la ley de protección de datos. Me pasó algo similar en 2019, cuando casi me hackean una app de gestión médica. Desde entonces, implemento medidas más robustas. La paranoia también es mi amiga!

Responder a Roberto Rodríguez

¡Enlace copiado al portapapeles!