La Superficie de Ataque de las Aplicaciones Web
Cómo Funciona una Aplicación Web
Una aplicación web es, en esencia, una conversación entre un cliente (el navegador) y un servidor a través del protocolo HTTP. El navegador envía una request (petición) y el servidor responde con una response (respuesta). Cada request incluye un método (GET, POST, PUT, DELETE), una URL, un conjunto de cabeceras y, opcionalmente, un cuerpo con datos. La response incluye un código de estado (200, 404, 500), cabeceras y normalmente un cuerpo HTML, JSON o de otro tipo.
Lo fundamental para la seguridad es que HTTP es un protocolo sin estado (stateless): el servidor no recuerda nada entre una request y la siguiente. Cada petición es independiente y se interpreta por sí sola. Esto significa que toda la información que el atacante quiera manipular viaja en algún lugar de la request: la URL, los parámetros de la query string, las cabeceras, las cookies o el cuerpo. Si tu aplicación confía ciegamente en cualquiera de esos datos, ahí nace una vulnerabilidad.
Conviene visualizar la anatomía de una petición real. Una request HTTP cruda tiene esta forma:
POST /api/checkout HTTP/1.1
Host: tienda.example.com
Cookie: session=8f3b2a...; theme=dark
Content-Type: application/json
Authorization: Bearer eyJhbGci...
{ "productId": 42, "quantity": 2 }
Cada línea de ese bloque es un canal de entrada controlado por el cliente: la ruta, cada cabecera, cada cookie y todo el cuerpo JSON. El servidor no tiene forma de saber si esos valores fueron generados por tu interfaz legítima o por una herramienta que los fabricó a mano. Por eso decimos que cada entrada es un punto de ataque potencial: la seguridad de la aplicación depende de qué hace el servidor con esos datos, no de cómo se supone que el navegador los produjo.
Cookies, Sesiones y Estado
Como HTTP no tiene estado, las aplicaciones necesitan un mecanismo para reconocer a un usuario entre peticiones. La solución más común son las cookies de sesión. Tras iniciar sesión, el servidor genera un identificador de sesión aleatorio, lo guarda y se lo envía al navegador mediante la cabecera Set-Cookie. En cada request posterior el navegador devuelve esa cookie, y el servidor la usa para recuperar el estado del usuario.
Aquí aparece una superficie de ataque enorme. Si el identificador de sesión es predecible, un atacante puede adivinarlo. Si viaja sin cifrar, puede interceptarlo en una red hostil. Si la cookie no tiene los atributos HttpOnly, Secure y SameSite, queda expuesta a robo mediante JavaScript malicioso o a ataques CSRF. Numerosas brechas históricas se reducen a esto: cookies de sesión sin HttpOnly que un script inyectado leyó y exfiltró, permitiendo al atacante suplantar por completo a la víctima sin conocer nunca su contraseña.
Veamos el error más común. Una cookie de sesión emitida sin atributos de seguridad es vulnerable:
// VULNERABLE: cookie de sesión sin protección
app.post('/login', (req, res) => {
const sessionId = createSession(req.body.user);
// Sin HttpOnly, sin Secure, sin SameSite:
// legible por JavaScript, viaja por HTTP plano y se envía cross-site
res.setHeader('Set-Cookie', `session=${sessionId}`);
res.json({ ok: true });
});
La versión segura declara explícitamente los atributos que reducen la superficie de ataque:
// SEGURO: la cookie queda fuera del alcance de JavaScript y de HTTP plano
app.post('/login', (req, res) => {
const sessionId = createSession(req.body.user);
res.cookie('session', sessionId, {
httpOnly: true, // inaccesible desde document.cookie -> frena el robo por XSS
secure: true, // solo se envía sobre HTTPS -> frena la intercepción
sameSite: 'lax', // no se manda en requests cross-site -> mitiga CSRF
maxAge: 1000 * 60 * 60, // vida acotada de la sesión
});
res.json({ ok: true });
});
HttpOnly impide que document.cookie lea el valor, de modo que un script inyectado no puede robar la sesión. Secure garantiza que la cookie nunca viaje por HTTP en claro. SameSite controla si la cookie se adjunta en peticiones originadas por otros sitios, que es la base de la protección contra CSRF. Entender bien este ciclo de vida es la base para razonar sobre el secuestro de sesión o la fijación de sesión, que veremos en la lección de autenticación.
El Modelo de Confianza Cliente-Servidor
Uno de los principios de seguridad más importantes es: nunca confíes en el cliente. Todo lo que ocurre en el navegador está bajo control del usuario. Las validaciones en JavaScript, los campos ocultos de un formulario, los precios mostrados en una página o los menús deshabilitados pueden ser modificados trivialmente con las DevTools del navegador o con un proxy de interceptación.
Esto implica que cualquier control de seguridad debe aplicarse en el servidor. La validación del lado del cliente es útil para la experiencia de usuario, pero no es una medida de seguridad. Un atacante puede saltarse por completo el navegador y enviar requests construidas a mano directamente al backend. Toda la lógica de autorización, validación de datos y reglas de negocio debe vivir en el servidor, donde el usuario no puede manipularla.
El caso clásico es un endpoint que confía en un valor sensible enviado por el cliente. Este código de pago confía en el precio y en el rol que llegan en el cuerpo de la request:
// VULNERABLE: el servidor confía en datos que controla el cliente
app.post('/api/checkout', (req, res) => {
const { productId, price, role } = req.body;
// El cliente puede enviar price: 0 o role: 'admin'
if (role === 'admin') applyAdminDiscount();
chargeCard(req.user, price); // precio dictado por el atacante
createOrder(productId, price);
res.json({ charged: price });
});
Un usuario malicioso simplemente cambia price a 0 o role a admin en la petición. La versión segura ignora esos datos y los deriva en el servidor desde fuentes de confianza:
// SEGURO: el servidor recalcula el precio y deriva el rol de su propia fuente
app.post('/api/checkout', async (req, res) => {
const { productId, quantity } = req.body;
// Validar la forma y el tipo de la entrada
if (!Number.isInteger(productId) || !Number.isInteger(quantity) || quantity < 1) {
return res.status(400).json({ error: 'Entrada inválida' });
}
// El precio proviene de la base de datos, nunca del cliente
const product = await db.products.findById(productId);
if (!product) return res.status(404).json({ error: 'Producto inexistente' });
// El rol proviene de la sesión del servidor, no del cuerpo de la request
const role = await getUserRole(req.session.userId);
const unitPrice = role === 'admin' ? product.adminPrice : product.price;
const total = unitPrice * quantity;
await chargeCard(req.session.userId, total);
await createOrder(productId, quantity, total);
res.json({ charged: total });
});
La diferencia pedagógica es clave: el cliente puede pedir comprar un producto, pero el precio y el rol los decide el servidor consultando fuentes que el atacante no controla. Ese es el corazón del modelo de confianza.
Mapeando la Superficie de Ataque
La superficie de ataque es el conjunto de todos los puntos donde un atacante puede intentar introducir o extraer datos. En una aplicación web típica incluye cada parámetro de URL, cada campo de formulario, cada cabecera HTTP, cada cookie, cada endpoint de API, cada archivo que se puede subir y cada integración con servicios externos.
Para entender la postura de seguridad de una aplicación es esencial enumerar esta superficie de forma sistemática. Los profesionales de seguridad usan un proxy de interceptación como Burp Suite o OWASP ZAP, que se sitúa entre el navegador y el servidor y permite ver y modificar cada request y response. Esto revela parámetros ocultos, endpoints no documentados y flujos de datos que no son visibles desde la interfaz. Las DevTools del navegador (pestañas Network y Application) complementan este trabajo mostrando cabeceras, cookies y llamadas de fondo. Cuanto mayor sea la superficie de ataque, más cuidado hay que poner en proteger cada entrada, por lo que minimizar la superficie —eliminar endpoints muertos, deshabilitar métodos HTTP no usados, no exponer campos innecesarios— es en sí mismo una defensa.
Estas herramientas son legítimas y se usan tanto en evaluaciones autorizadas como en el propio desarrollo: correr tu aplicación tras un proxy durante las pruebas te muestra exactamente qué datos salen y entran, y con frecuencia expone confianza indebida en el cliente que no era evidente desde el código.
Defensa en Profundidad como Marco
Ninguna defensa única es suficiente. El principio de defensa en profundidad consiste en aplicar múltiples capas de control de seguridad, de modo que si una falla, otra detenga el ataque. En la práctica esto significa combinar validación de entrada del lado servidor, codificación de salida, autenticación robusta, control de acceso estricto, transporte cifrado con HTTPS/TLS, configuración segura y monitoreo. Si un atacante evade la validación de un endpoint, la cookie con HttpOnly limita el daño; si roba una sesión, el control de acceso por rol frena la escalada. Cada capa asume que las anteriores podrían fallar.
A lo largo de este curso usaremos el OWASP Top 10 como mapa de los riesgos más críticos. Es una lista mantenida por la comunidad que recopila las categorías de vulnerabilidad más frecuentes e impactantes en aplicaciones web. Cada vulnerabilidad que estudiemos se relaciona también con un identificador CWE (Common Weakness Enumeration), el catálogo estándar de debilidades de software. Tener este marco mental claro te permitirá razonar de forma estructurada sobre cualquier aplicación que necesites proteger.
Checklist de Prevención
- Trata toda entrada del cliente (URL, query string, cabeceras, cookies, cuerpo) como no confiable y valídala en el servidor antes de usarla.
- Nunca confíes en precios, roles, permisos o identidades enviados por el cliente: derívalos o recalcúlalos en el backend desde fuentes de confianza (base de datos, sesión).
- Recuerda que la validación en JavaScript del navegador es solo para UX; duplica siempre esa validación en el servidor.
- Emite las cookies de sesión con
HttpOnly,SecureySameSite, y con un tiempo de vida acotado. - Fuerza HTTPS/TLS en todo el tráfico para que cookies y credenciales nunca viajen en claro.
- Usa identificadores de sesión largos y generados con un generador criptográficamente seguro.
- Minimiza la superficie de ataque: elimina endpoints muertos, deshabilita métodos HTTP no usados y no expongas campos ni parámetros innecesarios.
- Enumera sistemáticamente todas tus entradas y endpoints, apoyándote en proxies como Burp Suite u OWASP ZAP y en las DevTools.
- Aplica defensa en profundidad: no dependas de un único control; superpón validación, autenticación, autorización y monitoreo.
- Usa el OWASP Top 10 y los identificadores CWE como marco para razonar y priorizar riesgos de forma estructurada.