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:
| Propiedad | Significa | Se rompe cuando |
|---|---|---|
| Confidencialidad | Solo acceden quienes deben | Se filtran datos o se accede a recursos ajenos |
| Integridad | Los datos no se alteran sin permiso | Alguien modifica un precio, un saldo o un registro |
| Disponibilidad | El servicio funciona cuando se necesita | Denegació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:
- Dibujar el sistema: un diagrama de flujo de datos con actores, procesos, almacenes y flujos.
- Marcar las fronteras de confianza: allí donde los datos pasan de un nivel de confianza a otro.
- Enumerar amenazas por cada elemento y cada frontera.
- Decidir la respuesta: mitigar, eliminar la función, transferir el riesgo o aceptarlo de forma explícita.
- Verificar que las mitigaciones existen y revisar el modelo cuando el diseño cambia.
Fuera de la frontera
Aquí se valida, se autentica y se autoriza.
STRIDE es una lista de comprobación de seis familias de amenazas, cada una asociada a una propiedad:
| Amenaza | Propiedad vulnerada | Mitigación típica |
|---|---|---|
| Suplantación (Spoofing) | Autenticidad | Autenticación fuerte, MFA, firmas |
| Manipulación (Tampering) | Integridad | Validación, firmas, controles de acceso, TLS |
| Repudio (Repudiation) | Trazabilidad | Registros de auditoría protegidos |
| Fuga de información (Information disclosure) | Confidencialidad | Cifrado, mínimo privilegio, errores sin detalle |
| Denegación de servicio (Denial of service) | Disponibilidad | Límites de uso, colas, escalado |
| Elevación de privilegios (Elevation of privilege) | Autorización | Comprobaciones 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.
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.
Falla: 401, no autenticado.
Falla: 403, sin permiso.
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ón | Característica |
|---|---|
| Argon2id | Recomendada con carácter general; coste ajustable en memoria y tiempo |
| bcrypt | Muy extendida y válida; límite de longitud de entrada |
| scrypt | Coste en memoria; menos habitual |
| PBKDF2 | Aceptable 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 servidor | Token firmado (JWT) | |
|---|---|---|
| Qué viaja | Identificador opaco en una cookie | Datos firmados, autocontenidos |
| Revocación | Inmediata: se borra la sesión | Difícil hasta que caduca |
| Estado | En el servidor | En el cliente |
| Riesgo típico | Fijación y robo de cookie | Mala validación, caducidad larga, almacenamiento en el navegador |
| Encaja en | Aplicaciones web con interfaz propia | APIs entre servicios, delegación |
Se regenera tras el login para evitar la fijación de sesión.
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.
Lleva un valor de estado que la aplicación comprueba.
- El flujo recomendado para aplicaciones web y móviles es el código de autorización con PKCE.
- El parámetro
statevincula 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.
Se comprueba en el servidor, en cada acceso, nunca solo en la interfaz.
| Modelo | Decide por | Cuándo |
|---|---|---|
| RBAC (por roles) | Rol asignado al usuario | Permisos estables agrupados en pocos perfiles |
| ABAC (por atributos) | Atributos de usuario, recurso y contexto | Reglas finas: departamento, horario, propietario |
| ReBAC (por relaciones) | Relación entre usuario y recurso | Compartir 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
Vulnerable: el intérprete no distingue dato de instrucción.
Separar datos de 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.
Además, cambios de estado solo con POST, PUT o DELETE.
- Cookies con
SameSite=LaxoStrictcomo 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
Authorizationexplí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
| Cabecera | Función |
|---|---|
| Content-Security-Policy | Limita 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: nosniff | Impide que el navegador adivine el tipo de contenido |
| Referrer-Policy | Controla qué información de origen se envía |
| Permissions-Policy | Restringe 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-Policy | Aí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-originUna 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.
| Necesidad | Primitiva | Ejemplo |
|---|---|---|
| Confidencialidad entre partes con clave compartida | Cifrado simétrico autenticado | AES-GCM, ChaCha20-Poly1305 |
| Intercambio de claves, firma, identidad | Criptografía asimétrica | Curvas elípticas (Ed25519, ECDSA, X25519), RSA |
| Integridad y huella de datos | Función hash | SHA-256, SHA-3 |
| Autenticidad con secreto compartido | MAC | HMAC-SHA-256 |
| Contraseñas | Hash lento con sal | Argon2id, bcrypt |
| Números impredecibles | Generador criptográfico | El 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.randomo 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.
- 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" >> .gitignoreCadena 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
Se preservan las evidencias antes de limpiar.
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
| Técnica | Qué analiza | Cuándo | Limitación |
|---|---|---|---|
| SAST | El código fuente, sin ejecutarlo | En cada cambio | Falsos positivos; no ve la configuración real |
| SCA | Dependencias y sus vulnerabilidades conocidas | En cada cambio y de forma periódica | Solo conoce lo ya publicado |
| DAST | La aplicación en ejecución, desde fuera | En entornos de pruebas | Cobertura limitada a lo que alcanza |
| Análisis de secretos | Credenciales en código e historial | Antes de integrar | Patrones; no detecta todo |
| Revisión de código | Lógica, autorización y diseño | En cada cambio sensible | Depende del revisor |
| Pruebas de penetración | Intento controlado de explotar | Periódico y ante cambios grandes | Una 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:
| Principio | Consecuencia técnica |
|---|---|
| Minimización | Recoger solo los datos necesarios para la finalidad |
| Limitación de la finalidad | No reutilizar datos para fines distintos sin base legal |
| Limitación del plazo de conservación | Políticas de retención y borrado automático |
| Integridad y confidencialidad | Cifrado, control de acceso, copias protegidas |
| Privacidad desde el diseño y por defecto | La opción más protectora es la predeterminada |
| Derechos de las personas | Acceso, 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,SecureySameSite; 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».