Saltar al contenido principal
Jordi Izquierdo Montalbán
Lo que practico

Fundamentos

Seguridad

Valido toda entrada, aplico mínimo privilegio y compruebo mis controles.

Empezar por el modelo, no por la lista

La seguridad de aplicaciones (AppSec) no es una colección de parches: es decidir qué hay que proteger, de quién y con qué coste, y comprobar que las decisiones se cumplen en el código. Se organiza alrededor de tres preguntas: qué puede salir mal, qué se hace al respecto y cómo se sabe que funciona.

La tríada CIA da el vocabulario de lo que se protege:

PropiedadSignificaSe rompe cuando
ConfidencialidadSolo acceden quienes debenSe filtran datos o se accede a recursos ajenos
IntegridadLos datos no se alteran sin permisoAlguien modifica un precio, un saldo o un registro
DisponibilidadEl servicio funciona cuando se necesitaDenegación de servicio, borrado, cifrado malicioso

A ellas se suelen añadir autenticidad (saber quién es quién) y trazabilidad (poder reconstruir qué pasó).

Modelado de amenazas

Modelar amenazas es analizar el diseño antes de escribir el código para encontrar fallos cuando aún son baratos de corregir. El proceso básico:

  1. Dibujar el sistema: un diagrama de flujo de datos con actores, procesos, almacenes y flujos.
  2. Marcar las fronteras de confianza: allí donde los datos pasan de un nivel de confianza a otro.
  3. Enumerar amenazas por cada elemento y cada frontera.
  4. Decidir la respuesta: mitigar, eliminar la función, transferir el riesgo o aceptarlo de forma explícita.
  5. Verificar que las mitigaciones existen y revisar el modelo cuando el diseño cambia.
Cada vez que los datos cruzan una frontera de confianza hay que validarlos y autenticar.

STRIDE es una lista de comprobación de seis familias de amenazas, cada una asociada a una propiedad:

Cada letra apunta a una propiedad de seguridad que se puede romper.
AmenazaPropiedad vulneradaMitigación típica
Suplantación (Spoofing)AutenticidadAutenticación fuerte, MFA, firmas
Manipulación (Tampering)IntegridadValidación, firmas, controles de acceso, TLS
Repudio (Repudiation)TrazabilidadRegistros de auditoría protegidos
Fuga de información (Information disclosure)ConfidencialidadCifrado, mínimo privilegio, errores sin detalle
Denegación de servicio (Denial of service)DisponibilidadLímites de uso, colas, escalado
Elevación de privilegios (Elevation of privilege)AutorizaciónComprobaciones en el servidor, mínimo privilegio

Para priorizar se estima probabilidad por impacto con una escala acordada; lo importante es que la decisión quede escrita y tenga responsable. Un modelo de amenazas útil cabe en una o dos páginas y se actualiza con el diseño.

Marcos de referencia: OWASP Top 10 y ASVS

  • OWASP Top 10 es un documento de concienciación con las categorías de riesgo más frecuentes en aplicaciones web (control de acceso roto, fallos criptográficos, inyección, diseño inseguro, configuración incorrecta, componentes vulnerables, fallos de autenticación, integridad del software y de los datos, registro y monitorización insuficientes, SSRF). Sirve para entender el paisaje, no como lista de verificación completa.
  • ASVS (Application Security Verification Standard) es un catálogo de requisitos verificables, organizado por áreas y por niveles de rigor. Es la base adecuada para escribir requisitos de seguridad y para planificar una revisión.

Cómo usarlos: el Top 10 para formar al equipo; el ASVS para decidir el nivel exigible según la criticidad de la aplicación y verificar requisito a requisito.

Ninguna capa es perfecta: se apilan para que el fallo de una no baste.

La defensa en profundidad parte de que cualquier control puede fallar: validación, autenticación, autorización, cifrado, registro y red actúan en capas, de modo que un fallo en una no entrega todo.

Autenticación

Autenticar es comprobar que quien se presenta es quien dice ser.

Son dos preguntas distintas y se responden en momentos distintos.

Contraseñas

Las contraseñas nunca se guardan en claro ni con un hash rápido (como SHA-256 sin más). Se usa una función diseñada para ser costosa, con sal única por usuario:

FunciónCaracterística
Argon2idRecomendada con carácter general; coste ajustable en memoria y tiempo
bcryptMuy extendida y válida; límite de longitud de entrada
scryptCoste en memoria; menos habitual
PBKDF2Aceptable cuando se exige por normativa; menos resistente a hardware especializado
import argon2 from "argon2";
 
// Registro: se guarda solo el resultado, que incluye sal y parámetros
const hash = await argon2.hash(passwordIntroducida, { type: argon2.argon2id });
 
// Acceso: la comparación la hace la propia librería
const valida = await argon2.verify(hash, passwordIntroducida);

Buenas prácticas: permitir contraseñas largas (frases), comprobar contra listas de contraseñas filtradas, no imponer reglas arbitrarias de composición ni caducidad periódica sin motivo, limitar intentos y mensajes de error que no revelen si el usuario existe.

Segundo factor y passkeys

  • TOTP: códigos de seis cifras de un solo uso que dependen de un secreto compartido y del tiempo. Mejora mucho sobre solo contraseña, pero se puede reenviar a una web falsa (phishing en tiempo real).
  • SMS: mejor que nada, vulnerable al intercambio de SIM; se reserva para recuperación de bajo riesgo.
  • Claves de seguridad y passkeys (WebAuthn/FIDO2): el dispositivo guarda una clave privada que nunca sale de él y el servidor solo conoce la pública. La respuesta está ligada al dominio, por lo que resisten el phishing. Es el camino recomendado.
  • La recuperación de cuenta es parte de la superficie de ataque: un restablecimiento débil anula cualquier segundo factor.

Sesiones y tokens

Tras autenticar hay que recordar al usuario entre peticiones.

Sesión en servidorToken firmado (JWT)
Qué viajaIdentificador opaco en una cookieDatos firmados, autocontenidos
RevocaciónInmediata: se borra la sesiónDifícil hasta que caduca
EstadoEn el servidorEn el cliente
Riesgo típicoFijación y robo de cookieMala validación, caducidad larga, almacenamiento en el navegador
Encaja enAplicaciones web con interfaz propiaAPIs entre servicios, delegación
El navegador solo guarda un identificador opaco; el estado vive en el servidor.

Cookies de sesión: HttpOnly (el script no la lee), Secure (solo por HTTPS) y SameSite=Lax o Strict (no se envía en peticiones entre sitios), con caducidad razonable y regeneración del identificador al iniciar sesión y al cambiar de privilegios.

Si se usan JWT: validar firma y algoritmo esperado (rechazar none y no aceptar el algoritmo que indique el propio token), comprobar emisor, audiencia y caducidad, usar tokens de acceso de vida corta con renovación controlada, y no meter en ellos datos sensibles, porque están codificados, no cifrados.

OAuth 2.0 y OpenID Connect

OAuth 2.0 es un protocolo de autorización delegada: permite que una aplicación acceda a recursos en nombre del usuario sin conocer su contraseña. OpenID Connect (OIDC) añade una capa de identidad (el token de identidad) para iniciar sesión con un proveedor.

La aplicación obtiene un token sin ver nunca la contraseña del usuario.
  • El flujo recomendado para aplicaciones web y móviles es el código de autorización con PKCE.
  • El parámetro state vincula la respuesta con la petición y evita CSRF en el flujo.
  • Se validan la URL de retorno exacta (sin comodines), el emisor, la audiencia y el nonce.
  • Se piden alcances (scopes) mínimos.
  • No usar el flujo implícito ni guardar tokens de larga duración en almacenamiento accesible a scripts.
  • Autenticación (quién eres) y autorización (qué puedes) siguen siendo decisiones de la aplicación: el token no las sustituye.

Autorización

La mayoría de los fallos graves no son de autenticación, sino de control de acceso roto: el usuario está identificado, pero la aplicación no comprueba que pueda hacer esto con este recurso.

Autenticar dice quién eres; autorizar decide si puedes hacer esto con este recurso.
ModeloDecide porCuándo
RBAC (por roles)Rol asignado al usuarioPermisos estables agrupados en pocos perfiles
ABAC (por atributos)Atributos de usuario, recurso y contextoReglas finas: departamento, horario, propietario
ReBAC (por relaciones)Relación entre usuario y recursoCompartir documentos, jerarquías

IDOR / BOLA (referencia directa insegura a objetos) es el fallo más común: la API acepta un identificador y devuelve el recurso sin comprobar que pertenece a quien lo pide.

// Vulnerable: cualquiera puede leer cualquier pedido cambiando el identificador
app.get("/pedidos/:id", async (req, res) => {
  const pedido = await db.pedidos.findById(req.params.id);
  res.json(pedido);
});
 
// Correcto: el recurso se busca siempre acotado al usuario autenticado
app.get("/pedidos/:id", requiereSesion, async (req, res) => {
  const pedido = await db.pedidos.findOne({ id: req.params.id, usuarioId: req.usuario.id });
  if (!pedido) return res.status(404).end();
  res.json(pedido);
});

Reglas: comprobar en el servidor y en cada acceso; denegar por defecto; no fiarse de campos ocultos ni de roles enviados por el cliente; vigilar la asignación masiva (que el cliente pueda enviar campos como esAdmin); y distinguir los permisos a nivel de función (¿puede llamar a este endpoint?) de los de objeto (¿puede ver este registro?).

Inyección: mezclar datos con instrucciones

El fallo aparece cuando los datos se mezclan con las instrucciones.

La causa común de todas las inyecciones es que el intérprete recibe una cadena que mezcla instrucciones y datos del usuario. La solución común es mantenerlos separados.

Inyección SQL

// Vulnerable: el dato forma parte de la instrucción
const q = "SELECT * FROM usuarios WHERE email = '" + email + "' AND activo = true";
 
// Correcto: consulta parametrizada, el valor nunca se interpreta como SQL
const r = await db.query("SELECT * FROM usuarios WHERE email = $1 AND activo = true", [email]);

Los parámetros no sirven para nombres de tabla o columna: en esos casos se usa una lista cerrada de valores permitidos. Un ORM protege solo si no se concatenan fragmentos de texto en sus consultas en bruto.

Inyección de comandos

// Vulnerable: la entrada llega a un intérprete de comandos
exec("convert " + nombreArchivo + " salida.png");
 
// Correcto: sin intérprete, los argumentos van separados
execFile("convert", [nombreArchivo, "salida.png"]);

Mejor aún: usar una librería en lugar de invocar un programa externo, y validar el nombre contra una lista de caracteres permitidos.

XSS (Cross-Site Scripting)

El XSS ocurre cuando datos no confiables se insertan en una página y el navegador los ejecuta como código. Variantes: almacenado, reflejado y basado en DOM.

// Vulnerable: el texto se interpreta como HTML
contenedor.innerHTML = comentario;
 
// Correcto: se inserta como texto
contenedor.textContent = comentario;

Defensas: codificar según el contexto (HTML, atributo, URL, JavaScript), usar frameworks que escapan por defecto y evitar sus vías de escape (dangerouslySetInnerHTML), sanear con una librería de confianza el HTML que deba admitirse, y reforzar con CSP.

Inyección de plantillas (SSTI)

Ocurre cuando la entrada del usuario se usa como plantilla en el motor de plantillas del servidor, en lugar de como dato. Se evita no construyendo plantillas con texto del usuario: la plantilla es fija y los datos se pasan como variables.

Otras variantes

Inyección en consultas LDAP, XPath, cabeceras HTTP (saltos de línea) y en consultas de bases NoSQL por objetos. La misma regla: estructura fija, valores separados, validación por lista de permitidos.

CSRF y SSRF

CSRF aprovecha que el navegador adjunta las cookies automáticamente: un sitio ajeno hace que el navegador de la víctima envíe una petición que cambia estado.

El navegador adjunta las cookies solas; la defensa es exigir algo que un tercero no pueda enviar.
  • Cookies con SameSite=Lax o Strict como primera barrera.
  • Token anti-CSRF impredecible, ligado a la sesión, exigido en cada petición que cambia estado.
  • Cambios de estado solo con métodos no seguros (nunca con GET) y comprobación de Origin.
  • Las APIs que usan cabecera Authorization explícita (no cookies) no tienen este problema.

SSRF hace que el servidor realice peticiones a destinos elegidos por el atacante. Defensas: lista de destinos permitidos, resolución y rechazo de rangos privados, ausencia de redirecciones automáticas, red de salida restringida y acceso a metadatos de la nube protegido. El detalle de red está en la práctica de redes.

Cabeceras de seguridad

CabeceraFunción
Content-Security-PolicyLimita desde dónde se cargan scripts, estilos e imágenes; mitiga XSS
Strict-Transport-Security (HSTS)Obliga al navegador a usar siempre HTTPS
X-Content-Type-Options: nosniffImpide que el navegador adivine el tipo de contenido
Referrer-PolicyControla qué información de origen se envía
Permissions-PolicyRestringe funciones del navegador (cámara, geolocalización)
frame-ancestors (en CSP)Decide quién puede incrustar la página, contra el clickjacking
Cross-Origin-Opener-PolicyAísla el contexto de navegación de otros orígenes
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin

Una CSP eficaz evita unsafe-inline y unsafe-eval en scripts y se adopta primero en modo de informe (report-only) para ajustar sin romper la aplicación. Las cabeceras son una capa de refuerzo, no sustituyen a la codificación de salida.

Criptografía aplicada

La regla de oro: no inventar criptografía y no implementarla a mano; usar librerías y primitivas estándar bien mantenidas.

NecesidadPrimitivaEjemplo
Confidencialidad entre partes con clave compartidaCifrado simétrico autenticadoAES-GCM, ChaCha20-Poly1305
Intercambio de claves, firma, identidadCriptografía asimétricaCurvas elípticas (Ed25519, ECDSA, X25519), RSA
Integridad y huella de datosFunción hashSHA-256, SHA-3
Autenticidad con secreto compartidoMACHMAC-SHA-256
ContraseñasHash lento con salArgon2id, bcrypt
Números impredeciblesGenerador criptográficoEl del sistema operativo

Qué no hacer:

  • Usar MD5 o SHA-1 para seguridad, o un hash rápido para contraseñas.
  • Cifrar sin autenticar (modos sin integridad, como ECB o CBC sin MAC).
  • Reutilizar un nonce o un vector de inicialización con la misma clave.
  • Generar tokens con Math.random o con valores predecibles (tiempo, contadores).
  • Comparar secretos con ==: se usa comparación en tiempo constante.
  • Guardar la clave junto a los datos que protege, o dejarla en el repositorio.
  • Confundir codificación (Base64) con cifrado: Base64 no protege nada.
import { randomBytes, timingSafeEqual } from "node:crypto";
 
// Token impredecible para restablecer contraseña
const token = randomBytes(32).toString("base64url");
 
// Comparación en tiempo constante de dos valores del mismo tamaño
const iguales = timingSafeEqual(Buffer.from(a), Buffer.from(b));

Gestión de secretos

Claves de API, contraseñas de bases de datos y claves de firma son el objetivo más rentable para un atacante.

Un secreto bien gestionado se inyecta en ejecución, se rota y se puede revocar.
  • Nunca en el repositorio ni en imágenes de contenedor; tampoco en el historial de Git.
  • Se inyectan en ejecución desde un gestor de secretos o variables del entorno de despliegue.
  • Un secreto por entorno y por servicio, con permisos mínimos y rotación planificada.
  • Si un secreto se filtra: se revoca y se rota; borrar el commit no basta.
  • Análisis automático de secretos en cada commit y en el historial.
  • Los secretos no aparecen en los registros ni en los mensajes de error.
# Fichero de ejemplo versionado, sin valores reales
cat .env.example
# DATABASE_URL=
# API_KEY=
 
# El fichero con valores reales nunca se versiona
echo ".env" >> .gitignore

Cadena de suministro

La mayor parte del código de una aplicación moderna es de terceros.

  • Inventario de dependencias (SBOM) y análisis de composición (SCA) para detectar vulnerabilidades conocidas.
  • Fichero de bloqueo versionado e instalaciones reproducibles; actualizaciones revisadas.
  • Cuidado con los nombres parecidos (typosquatting), paquetes abandonados y scripts de instalación.
  • Dependencias mínimas: cada una amplía la superficie.
  • CI/CD como parte del perímetro: permisos mínimos para los trabajos, secretos de ámbito limitado, acciones y plantillas de terceros fijadas por versión exacta.
  • Imágenes de contenedor de origen fiable, mínimas y analizadas.
  • Verificar la integridad de lo que se descarga y firmar los artefactos propios.

Registro, auditoría y monitorización

Sin registros no se puede detectar ni investigar. Con registros mal diseñados se filtran datos.

  • Registrar eventos de seguridad: inicios de sesión (correctos y fallidos), cambios de privilegios, acciones administrativas, denegaciones de acceso y errores de validación.
  • Cada evento con marca de tiempo, actor, acción, recurso y resultado, y un identificador de correlación.
  • No registrar contraseñas, tokens, números de tarjeta ni datos personales innecesarios.
  • Neutralizar saltos de línea en lo que viene del usuario, para que no pueda falsificar entradas.
  • Almacenamiento centralizado, protegido contra modificación y con retención definida.
  • Alertas sobre patrones accionables (ráfagas de fallos de acceso, accesos fuera de patrón), no sobre todo.

Respuesta a incidentes

Un ciclo conocido y ensayado reduce el daño y el tiempo de recuperación.

La respuesta se prepara antes: quién decide, cómo se contacta, dónde están los registros, cómo se revocan credenciales y cómo se comunica. Durante el incidente se contiene primero, preservando evidencias, y se recupera después. Si hay datos personales afectados, la normativa puede imponer obligaciones de notificación con plazos, por lo que conviene saber de antemano quién las gestiona. Al terminar, una revisión sin buscar culpables convierte el incidente en mejoras concretas.

SDLC seguro: seguridad en cada fase

La seguridad se integra en cada fase, no se añade al final.
TécnicaQué analizaCuándoLimitación
SASTEl código fuente, sin ejecutarloEn cada cambioFalsos positivos; no ve la configuración real
SCADependencias y sus vulnerabilidades conocidasEn cada cambio y de forma periódicaSolo conoce lo ya publicado
DASTLa aplicación en ejecución, desde fueraEn entornos de pruebasCobertura limitada a lo que alcanza
Análisis de secretosCredenciales en código e historialAntes de integrarPatrones; no detecta todo
Revisión de códigoLógica, autorización y diseñoEn cada cambio sensibleDepende del revisor
Pruebas de penetraciónIntento controlado de explotarPeriódico y ante cambios grandesUna foto en el tiempo

Las herramientas automáticas encuentran patrones; los fallos de lógica de negocio y de autorización los encuentran el modelo de amenazas, las pruebas escritas para ellos y la revisión humana. Una lista de comprobación de revisión de código de seguridad: ¿quién puede llamar a esto?, ¿se comprueba la propiedad del recurso?, ¿qué entrada llega a un intérprete?, ¿qué se registra?, ¿qué ocurre si falla?

Privacidad y protección de datos (RGPD)

La seguridad protege los datos; la privacidad decide qué datos se tienen y para qué. Principios del RGPD con consecuencias de diseño:

PrincipioConsecuencia técnica
MinimizaciónRecoger solo los datos necesarios para la finalidad
Limitación de la finalidadNo reutilizar datos para fines distintos sin base legal
Limitación del plazo de conservaciónPolíticas de retención y borrado automático
Integridad y confidencialidadCifrado, control de acceso, copias protegidas
Privacidad desde el diseño y por defectoLa opción más protectora es la predeterminada
Derechos de las personasAcceso, rectificación, supresión, portabilidad y oposición

El derecho de supresión obliga a poder localizar y eliminar los datos de una persona en todos los sistemas, incluidos réplicas, índices de búsqueda y copias con sus plazos de rotación. Se facilita con identificadores de usuario consistentes, inventario de dónde se guardan los datos personales y seudonimización (separar la identidad de los datos de actividad). Un registro de tratamientos y las evaluaciones de impacto en los casos de riesgo alto forman parte de la documentación que acompaña a la parte técnica.

Lista de comprobación

  • Modelo de amenazas del sistema, con fronteras de confianza, revisado ante cambios.
  • Contraseñas con Argon2id o bcrypt; segundo factor o passkeys disponibles.
  • Autorización comprobada en el servidor, por recurso, con denegación por defecto.
  • Consultas parametrizadas y codificación de salida según el contexto.
  • Cookies con HttpOnly, Secure y SameSite; protección CSRF en cambios de estado.
  • CSP, HSTS y demás cabeceras configuradas y probadas.
  • Secretos fuera del código, rotables y con análisis automático.
  • Dependencias inventariadas, bloqueadas y analizadas.
  • Registro de eventos de seguridad sin datos sensibles; alertas accionables.
  • Plan de respuesta a incidentes ensayado.
  • Datos personales inventariados, con retención y proceso de supresión.

Antipatrones

  • «Seguridad por oscuridad»: esconder una ruta o confiar en que nadie conoce el identificador.
  • Validar solo en el cliente y asumir que el servidor recibe datos correctos.
  • Un único control (por ejemplo, el cortafuegos) como toda la defensa.
  • Mensajes de error con trazas, consultas SQL o rutas internas.
  • Roles y permisos decididos por un campo que envía el propio cliente.
  • Guardar tokens de larga duración en el almacenamiento del navegador accesible por scripts.
  • Dejar credenciales por defecto, cuentas de prueba y modos de depuración activos en producción.
  • Dejar la seguridad para «la última semana», o tratar los hallazgos de las herramientas como ruido.
  • Registrar todo, incluidos datos personales y secretos, «por si hace falta».