1Por qué importa
Casi todas las decisiones de seguridad de una aplicación web se toman leyendo campos de una petición HTTP: quién eres por una cookie, qué puedes hacer por una cabecera, desde dónde vienes por otra. Si no tienes claro cuáles de esos campos los controla el cliente y cuáles el intermediario, vas a confiar en el sitio equivocado. La lista de fallos que salen de ahí es larga y aburridamente repetida.
El malentendido más caro es tratar HTTP como si fuera una conversación directa entre el navegador y tu código. No lo es. Entre ambos hay una cadena de intermediarios, y cada uno reescribe cosas: el proxy inverso añade la dirección real del cliente en una cabecera, la red de distribución cachea respuestas según otras, el balanceador reparte según una tercera. Un fallo de seguridad clásico es que la aplicación confíe en una cabecera que un intermediario debía haber reescrito pero no lo hizo, dejando pasar el valor que puso el cliente.
Y hay una razón de método. Esta lección es la que conecta la fase de redes con la de seguridad web que viene después. Todo lo que verás en autenticación, control de acceso o inyección ocurre dentro de una petición HTTP. Aprender a leerla y a modificarla ahora convierte esas lecciones en algo que puedes reproducir con tus manos en vez de creer.
El entregable es un mapa de confianza de una petición: qué campo pone el cliente, qué campo pone cada intermediario, y en cuál de ellos puede confiar tu aplicación para tomar una decisión. Un campo que el cliente controla no puede sostener una decisión de autorización, y saber cuáles son esos campos es la mitad de la seguridad web.
2Conceptos
- Método:
GET,POST,PUT,DELETEy compañía; declara la intención de la petición. - Seguro: un método que no debería cambiar el estado del servidor, como
GET. - Idempotente: repetir la petición da el mismo resultado que hacerla una vez.
- Cabecera: un par nombre-valor con metadatos; las hay de petición, de respuesta y de intermediario.
- Cookie: estado que el servidor guarda en el cliente y que el cliente reenvía en cada petición al mismo origen.
- Proxy directo: intermediario del lado del cliente; habla en nombre del cliente hacia cualquier servidor.
- Proxy inverso: intermediario del lado del servidor; recibe por la aplicación y decide qué le pasa.
- Origen: la terna esquema, host y puerto; es la unidad sobre la que el navegador aplica sus reglas.
3Explicación profunda
Una petición HTTP es texto con estructura: una línea de petición con método, ruta y versión; un bloque de cabeceras; una línea en blanco; y un cuerpo opcional. Esa simplicidad es la que permite leerla y escribirla a mano, y es también la que hace que cualquier intermediario del camino pueda alterarla sin esfuerzo. No hay nada en el protocolo que ate una cabecera a quien la escribió.
Los métodos tienen propiedades que importan para seguridad más de lo que
parece. GET es seguro e idempotente: no debería cambiar nada, y por
eso mandar una operación con efecto por GET es un error clásico que
habilita ataques de petición forzada, porque un simple enlace o una imagen la
disparan. POST no es idempotente, y esa es la razón de que un doble
clic pueda cobrar dos veces. PUT y DELETE sí lo son.
Diseñar la semántica correcta no es purismo: decide qué ataques son posibles y
qué reintentos son seguros.
Las cabeceras hay que dividirlas en tres poblaciones y no mezclarlas
nunca. Las que pone el cliente y por tanto controla por completo:
User-Agent, Referer, cualquier cabecera personalizada.
Ninguna de ellas puede sostener una decisión de seguridad, porque el cliente
escribe lo que quiera. Las que pone el intermediario:
X-Forwarded-For con la dirección real, X-Forwarded-Proto
con el esquema original. Estas solo son fiables si tu aplicación únicamente es
alcanzable a través de ese intermediario; si además escucha directamente, el
cliente puede falsificarlas. Y las que pone el servidor en la respuesta,
que son las que configuran al navegador: Set-Cookie,
Content-Security-Policy, Strict-Transport-Security.
Las cookies son el mecanismo con más matices, porque cada atributo
protege contra un ataque distinto y omitir uno abre una puerta concreta.
HttpOnly impide que el código de la página lea la cookie, lo que
limita el daño de un XSS a robar la sesión. Secure impide que viaje
por texto claro, cerrando la captura en la red. SameSite controla
si la cookie se envía en peticiones que vienen de otro sitio, que es la defensa
de fondo contra la petición forzada. Un Domain demasiado amplio
comparte la cookie con subdominios que quizá no controlas. No hay un atributo
“seguro”: hay una combinación correcta para cada cookie según lo que guarde.
La distinción entre proxy directo y proxy inverso se entiende por la dirección de la confianza. Un proxy directo está del lado del cliente y actúa en su nombre: filtra su salida, oculta su origen, o intercepta su tráfico para inspeccionarlo. Un proxy inverso está del lado del servidor y protege la aplicación: termina el TLS, reparte carga, filtra peticiones y añade contexto. El mismo software hace de las dos cosas; lo que cambia es de qué lado de la frontera de confianza está y a quién representa. Confundirlos lleva a colocar el control en el sitio equivocado.
Una consecuencia que conviene interiorizar: la aplicación casi nunca ve al cliente de verdad. Ve lo que el último intermediario le contó del cliente. Por eso la seguridad web bien hecha define con precisión qué intermediario es de confianza, obliga a que todo el tráfico pase por él, y trata como sospechoso cualquier campo que ese intermediario no haya reescrito explícitamente.
4Ejemplo sencillo
# Una carta que pasa por varias manos metodo que quieres: "traeme" (GET), "guarda esto" (POST) cabeceras las notas al margen del sobre: idioma, remitente, sello cuerpo la carta dentro cookie una pulsera que te pusieron en la entrada y ensenas al volver # Quien escribe cada nota al margen el cliente pone su remitente. Puede escribir el que quiera. el proxy tacha el remitente y escribe la direccion real desde donde llego la carta. En eso SI se puede confiar... ...solo si NADIE puede meter cartas sin pasar por el. # Los atributos de la pulsera, uno por ataque HttpOnly la pulsera no se puede fotografiar desde dentro de la fiesta Secure la pulsera solo vale si entraste por la puerta buena SameSite la pulsera no funciona si te trajo alguien de otra fiesta
5Ejemplo técnico
Así se ve una petición cuando la lees desde el intermediario. Esta es la ruta
/eco del laboratorio, que devuelve lo que la aplicación recibió de
verdad, después de pasar por el proxy:
Mira la línea x-rol. El cliente envió usuario y la
aplicación recibió administrador, porque el intermediario reescribió
esa cabecera por el camino. Ese es el punto entero de la lección: una aplicación
que autorizara por X-Rol estaría confiando en algo que un
intermediario puede poner a voluntad, y en algo que el cliente puede falsificar
si consigue hablar con la aplicación sin pasar por el proxy.
| Campo | Quién lo controla | ¿Sostiene una decisión de seguridad? |
|---|---|---|
User-Agent, Referer | El cliente, por completo | No, nunca |
| Cabeceras personalizadas del cliente | El cliente | No |
X-Forwarded-For | El intermediario, si reescribe; el cliente, si no | Solo si la app es inalcanzable sin el proxy |
| Cookie de sesión | El servidor la emite; el cliente la reenvía | Sí, si está firmada o es opaca y bien atada |
Host | El cliente, y a menudo se usa para enrutar | No sin validación explícita contra una lista |
La tabla es el mapa de confianza que pedía el objetivo. La fila de
X-Forwarded-For es la que más incidentes causa: parece fiable
porque “la pone el proxy”, y deja de serlo en cuanto la aplicación escucha
también por un camino que no pasa por el proxy.
6Diagrama
Ver el mismo diagrama como código Mermaid (editable)
flowchart LR
C[cliente: pone lo que quiere] --> P[proxy inverso: reescribe]
P --> A[aplicacion: ve lo que le cuentan]
C -. camino directo a cerrar .-> A
7Código
La configuración de una cookie de sesión es donde esta lección se vuelve código. Estos son los dos extremos, y la diferencia entre ellos es una lista de ataques:
# Cookie floja: la que emite el laboratorio a proposito Set-Cookie: sesion=demo-de-laboratorio; Path=/ # Cookie endurecida: cada atributo cierra un ataque concreto Set-Cookie: sesion=<valor opaco y aleatorio>; HttpOnly; # el JS de la pagina no la lee: limita el XSS Secure; # no viaja por HTTP en claro SameSite=Lax; # no se envia en navegaciones desde otro sitio Path=/; # alcance minimo dentro del origen Max-Age=3600 # caduca: una sesion robada no es eterna
Cada atributo de la versión endurecida corresponde a una entrada de la fase de
seguridad web: HttpOnly aparece en la lección de XSS,
SameSite en la de petición forzada, Secure en la de
TLS que viene justo después de esta. Endurecer la cookie no es una casilla que
marcar: es entender de qué te protege cada atributo, y por eso omitir uno es una
decisión, no un olvido.
# Y la comprobacion, contra tu propio laboratorio for attr in HttpOnly Secure SameSite; do if curl -sI http://localhost:8092/login | grep -i Set-Cookie | grep -qi "$attr"; then echo "presente $attr" else echo "FALTA $attr" fi done
8Qué puede salir mal
Autorizar por una cabecera que el cliente controla. Decidir que
alguien es administrador porque envió X-Rol: administrador, o
confiar en X-Forwarded-For cuando la aplicación es alcanzable
sin pasar por el proxy. El cliente escribe esos valores.
Operaciones con efecto por GET. Un GET que
borra o transfiere se dispara con un enlace, una imagen o una precarga del
navegador. La semántica del método no es decorativa.
Cookies de sesión sin atributos. Sin HttpOnly un XSS se
lleva la sesión; sin Secure la captura de red la lee; sin
SameSite la petición forzada la usa. Tres puertas por omitir
tres palabras.
Confiar en la cabecera Host sin validarla. Muchas
aplicaciones construyen enlaces de recuperación de contraseña con el
Host que envió el cliente. Cambiarlo redirige el enlace hacia
un dominio del atacante.
9Cómo lo abusaría un atacante
Lo primero que hace alguien que ataca una web es exactamente lo que vas a hacer en el laboratorio: ponerse un intermediario propio entre su navegador y la aplicación, para ver la petición cruda y modificarla campo a campo. No para explotar nada todavía, sino para entender qué campos existen y cuáles la aplicación parece leer para decidir.
Después prueba la confianza mal colocada. Envía X-Forwarded-For
con una dirección interna, por si la aplicación cree que viene de dentro.
Cambia el Host, por si eso reescribe enlaces. Inventa cabeceras
de rol o de depuración, por si alguna quedó cableada. Ninguno de estos
intentos rompe nada; simplemente comprueban si la aplicación confía en
entradas que no debería, y con sorprendente frecuencia lo hace.
La tercera línea es la cookie. Si no tiene HttpOnly, cualquier
XSS la convierte en robo de sesión. Si no tiene SameSite, una
página del atacante puede disparar peticiones autenticadas en nombre de la
víctima. Y si no tiene Secure, basta con estar en la red, que
es justo lo que la fase anterior enseñó a conseguir. Las tres debilidades se
detectan mirando una sola cabecera de respuesta.
10Cómo defenderlo
- No autorices por cabeceras que el cliente controla. La autorización se decide con la sesión validada, no con
X-cualquier-cosa. - Haz que la aplicación solo sea alcanzable a través del proxy antes de confiar en una sola cabecera que el proxy reescriba.
- Respeta la semántica de los métodos. Lo que cambia estado va por
POST,PUToDELETE, nunca porGET. - Endurece cada cookie con la combinación que corresponde a lo que guarda, y comprueba los atributos con una prueba automática.
- Valida la cabecera
Hostcontra una lista de valores esperados antes de usarla para construir nada. - Añade las cabeceras de respuesta de seguridad en el proxy, para que apliquen a toda la aplicación sin depender de cada endpoint.
11Laboratorio
Levanta el laboratorio. Trae un cliente, un intermediario y la aplicación objetivo:
cd cursos/seguridad/labs/lab-net-proxy
docker compose up -d --build
docker compose ps
Primero mira la aplicación de frente, sin intermediario, y después a
través de él. Compara las dos salidas de /eco:
Ahora observa lo que el intermediario registra de ti, cookie incluida, y comprueba que la cookie de sesión sale sin ningún atributo de seguridad:
curl -s -c galletas.txt http://localhost:8092/login
curl -sI http://localhost:8092/login | grep -i set-cookie
curl -s -b galletas.txt http://localhost:8092/eco
docker compose logs --tail 5 intermediario
Y comprueba que el intermediario también modifica la respuesta, no solo
la petición. La ruta /saldo sale reescrita; la misma por el
camino directo, no:
curl -s http://localhost:8092/saldo
curl -s http://localhost:8092/directo/saldo
docker compose down -v
12Ejercicios
Clasifica cinco campos por confianza
Para cada uno, di quién lo controla y si tu aplicación podría usarlo para
una decisión de seguridad: (1) User-Agent;
(2) X-Forwarded-For con la app solo alcanzable por el proxy;
(3) el mismo con la app también alcanzable directamente; (4) el valor de una
cookie de sesión firmada; (5) la cabecera Host.
(1) Cliente, no sirve para nada de seguridad. (2) Intermediario fiable, porque no hay otro camino: se puede usar con cuidado. (3) Cliente disfrazado de intermediario, no sirve: el cliente puede ponerla al hablar directo con la app. (4) Servidor, sirve: la firma es lo que ata el valor a que lo emitió el servidor. (5) Cliente, no sirve sin validarla contra una lista de hosts esperados.
Clave: cliente · fiable · no fiable · fiable · no sin validar. El par (2)/(3) está bien resuelto solo si das respuestas distintas.
Endurece la cookie del laboratorio
La ruta /login emite una cookie sin ningún atributo. Modifica
la configuración de la aplicación en el laboratorio para que emita la cookie
endurecida, y demuestra con curl que los tres atributos están
presentes. Explica qué ataque cierra cada uno.
conf/app.conf, en la línea add_header Set-Cookie.up -d.curl -sI sobre /login; el atributo Secure es honesto solo si de verdad sirves por HTTPS, así que aquí anótalo como pendiente para la lección siguiente.# en conf/app.conf, ruta /login add_header Set-Cookie "sesion=demo-de-laboratorio; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600";
HttpOnly impide que un XSS lea la cookie desde el
navegador; SameSite=Lax evita que la cookie viaje en
peticiones iniciadas por otro sitio, que es la base de la petición
forzada; Max-Age limita la vida de una sesión robada.
Secure queda pendiente porque el laboratorio sirve HTTP en
claro, y añadirlo aquí sería una casilla marcada sin efecto real: en
f02-tls ese servicio pasa a HTTPS y entonces el atributo
protege de verdad.
Clave: se evalúa que expliques por qué Secure no debe ponerse todavía. Marcarlo sin HTTPS es teatro de seguridad.
Investiga el contrabando de peticiones
Cuando dos intermediarios interpretan de forma distinta dónde acaba una petición, aparece una familia de ataques con nombre propio. Investiga fuera de esta lección y responde: ¿qué dos cabeceras entran en conflicto?, ¿por qué el desacuerdo entre proxy y aplicación permite “colar” una segunda petición?, y ¿qué defensa lo cierra de raíz?
Content-Length y Transfer-Encoding.
El conflicto es entre Content-Length, que declara el tamaño
del cuerpo en bytes, y Transfer-Encoding: chunked, que lo
declara por trozos. Cuando una petición trae las dos y el proxy hace
caso a una mientras la aplicación hace caso a la otra, discrepan sobre
dónde termina el cuerpo. Los bytes que para el proxy son el final del
cuerpo, para la aplicación son el comienzo de una segunda petición que
el proxy no llegó a ver, y que por tanto no pasó por ninguno de sus
controles.
La defensa de raíz es no permitir la ambigüedad: rechazar toda petición que traiga las dos cabeceras, normalizar el mensaje en el proxy antes de reenviarlo, y usar en lo posible protocolos donde el enmarcado no dependa de estas cabeceras. Es un buen recordatorio de que un intermediario no solo reescribe: también interpreta, y dos interpretaciones distintas son una vulnerabilidad.
Clave: Content-Length contra Transfer-Encoding. Lo evaluable es que expliques cómo el desacuerdo de enmarcado inyecta una petición no vista.
Red Team · falsifica la cabecera de confianza
Dentro del lab-net-proxy, y solo dentro: demuestra que la
cabecera X-Forwarded-For solo es fiable cuando la aplicación es
inalcanzable sin el proxy. Habla directamente con la aplicación, sáltate el
intermediario y consigue que /eco refleje una dirección que tú
elegiste.
cliente también está en la red interna.docker compose exec cliente curl -s -H "X-Forwarded-For: dentro-de-la-red" http://app/eco.
Hablando directamente con app, la cabecera
X-Forwarded-For que envía el cliente llega intacta, porque
no hay intermediario que la sobrescriba. La aplicación reflejará
la dirección que enviaste o lo que hayas puesto. La conclusión práctica es
dura: una aplicación que decida “esto viene de la red interna” a partir
de esa cabecera, y que además escuche por un camino que no pasa por el
proxy, está tomando decisiones de seguridad con datos del atacante.
Por eso la defensa no es “validar la cabecera”, que es imposible, sino
garantizar la topología: que la aplicación sea físicamente inalcanzable
salvo a través del proxy. En el laboratorio eso se ve porque
app no publica puertos, pero sí es alcanzable desde otros
contenedores de su red, que es exactamente el hueco.
Clave: el hallazgo no es la cabecera falsificada, es entender que la fiabilidad era una propiedad de la topología, no del proxy.
Blue Team · qué registra el intermediario y qué no
Mira el formato de log del intermediario en conf/intermediario.conf
y genera tráfico variado contra él. Responde: con lo que registra, ¿podrías
distinguir una petición legítima de una con una cabecera de rol falsificada?
¿Qué campos añadirías al log para poder investigar la falsificación del
ejercicio anterior?
El log del intermediario registra la petición, la cookie y varias
cabeceras, así que sobre el tráfico que pasa por él se puede investigar
bastante. El problema es que el ataque del ejercicio rojo no pasa
por el intermediario: habla directo con la aplicación, y por tanto
es invisible para ese log. La única forma de detectarlo es que la propia
aplicación registre el valor de X-Forwarded-For tal y como
le llega, y que ese log se compare con el del proxy: cualquier petición
que la app vio y el proxy no, es un salto.
El campo clave que falta, otra vez, es la correlación: un identificador que el proxy inyecte y la app registre. Sin él, tienes dos logs que no se pueden cruzar, y el salto se pierde entre ambos.
Clave: el entregable es la lista de campos que permiten cruzar el log del proxy con el de la app. Es la misma correlación que la fase 14 va a implementar.
Architecture Challenge · dónde vive cada control HTTP
Diseña dónde colocarías cada control de una aplicación web con un proxy inverso delante: terminación de TLS, cabeceras de respuesta de seguridad, limitación de tasa, autorización y validación de entrada. Justifica qué va en el proxy y qué va en la aplicación, y explica qué pasa si la aplicación queda alcanzable sin pasar por el proxy.
Una asignación razonable: en el proxy, la terminación de TLS, las cabeceras de respuesta de seguridad y una primera capa de limitación de tasa, porque son controles uniformes que conviene aplicar en un único sitio para toda la aplicación. En la aplicación, la autorización y la validación de entrada, porque necesitan el contexto de negocio que el proxy no tiene: qué recurso concreto se pide y qué le corresponde a este usuario. La validación de entrada, además, nunca se delega por completo, porque el proxy no conoce la forma de cada dato.
La condición que sostiene todo el diseño es que la aplicación sea inalcanzable salvo por el proxy. Si ese supuesto se rompe, cada control que estaba en el proxy desaparece para quien entra por el otro camino, y las cabeceras de confianza vuelven a estar en manos del cliente. Por eso la primera línea de esta arquitectura no es un control HTTP, es la segmentación de red que la fase siguiente construye.
Clave: no hay una única respuesta. Se evalúa que la autorización quede en la app, que las cabeceras uniformes queden en el proxy, y que nombres la topología como supuesto de todo lo demás.
13Preguntas de comprensión
Tu aplicación decide que alguien es administrador porque envió X-Rol: administrador. ¿Qué tiene de malo?
La autorización se decide con la sesión validada, no con un campo que el cliente escribe a voluntad.
¿Cuándo es fiable la cabecera X-Forwarded-For?
Su fiabilidad es una propiedad de la topología. Si existe un camino que salta el proxy, el cliente puede falsificarla.
¿Qué atributo de cookie limita el daño de un XSS a la sesión?
SameSite ataca la petición forzada y Secure la captura de red; el que corta la lectura desde el navegador es HttpOnly.
¿Por qué una operación que cambia estado no debe ir por GET?
GET es un método seguro por contrato: al usarlo para algo con efecto, cualquier cosa que cargue una URL ejecuta la operación.
14Reto adicional
Audita las cabeceras de un sitio tuyo
Toma una aplicación web que controles y audita sus cabeceras de respuesta.
Para cada una de las de seguridad —Content-Security-Policy,
Strict-Transport-Security, los atributos de las cookies—
responde: ¿está?, ¿con qué valor?, ¿qué ataque cubre y contra cuál te deja
expuesto su ausencia o su valor laxo?
curl -sI te da todas las cabeceras de respuesta de un vistazo.
El patrón habitual es que falten la mitad y que las presentes tengan
valores por defecto. La Content-Security-Policy suele estar
ausente o tan permisiva que no cubre el XSS que debería; la
Strict-Transport-Security a menudo falta, dejando abierta la
primera visita por HTTP; y las cookies casi siempre carecen de al menos
un atributo. Lo interesante es que este mismo curso predica con el
ejemplo: la CSP de estas páginas es la que estás auditando, y por eso el
contenido interactivo no usa scripts en línea.
El entregable no es la lista de lo que falta, sino qué ataque habilita cada ausencia. Una cabecera sin ese razonamiento es una recomendación copiada; con él, es una decisión de riesgo escrita con el vocabulario de la fase 01.
Clave: cada hallazgo debe llevar el ataque que abre. “Falta HSTS” no es un hallazgo; “sin HSTS, la primera visita por HTTP es interceptable” sí lo es.
15Resumen
- Una petición HTTP es texto con estructura y cualquier intermediario del camino puede leerla y reescribirla.
- Las cabeceras se dividen en las que pone el cliente, las que pone el intermediario y las que pone el servidor. Solo las terceras, y las de intermediario bajo condición, sostienen decisiones.
- La fiabilidad de una cabecera de intermediario depende de que no exista un camino que salte al intermediario. Es topología, no protocolo.
- Los métodos tienen semántica:
GETes seguro,POSTno es idempotente. Respetarla decide qué ataques son posibles. - Cada atributo de cookie cierra un ataque concreto. Omitir uno es una decisión, no un olvido, y
Securesin HTTPS es teatro. - Un proxy directo representa al cliente y uno inverso al servidor. La confianza va en direcciones opuestas.
X-Forwarded-For en fiable o en falsificable?SameSite en una cookie?16Mis notas
Estas notas se guardan solo en este navegador. No se envían a ningún servidor. Expórtalas desde el panel de progreso si cambias de máquina.
17Recursos
Los comentarios se cargan solo si los pides, desde GitHub Discussions. No se carga nada de terceros mientras no pulses.