Saltar al contenido
>_ITDITDPlataforma de seguridad web

Guías de seguridad

Qué es la suplantación de X-Forwarded-For (XFF) — la trampa de la configuración de proxy de confianza

X-Forwarded-For (XFF) es un encabezado que el cliente puede falsificar: los escáneres esconden sondeos de inyección en él y 'confiar en todos los proxies' deja pasar el valor falso. La trampa del proxy de confianza y cómo defenderse.

Publicado 2026-06-08 12 min de lectura

X-Forwarded-For (XFF) es el encabezado que le dice a tu app "la IP real del visitante es esta" cuando el tráfico pasa por un proxy. El truco: el cliente puede falsificar ese valor libremente. Aquí va un caso común de desarrollo independiente — "nos golpeó un escaneo de suplantación de XFF" — convertido en lección, anonimizado, sin pasos de ataque reproducibles.

Ficha del caso
Tipo
Suplantación de X-Forwarded-For + sondeo de inyección (escaneo automatizado)
Impacto
Ninguno (detenido por la defensa en profundidad)
Cómo salió a la luz
Las solicitudes de ataque dieron 500; una avalancha de correos de excepción
Escudos que aguantaron
① placeholders (valores enlazados) ② validación del charset de la BD
El hueco que quedó
"confiar en todos los proxies" — sin proxy realmente delante
Respuesta
Sanear los encabezados de IP en el límite (parche) → arreglar la config del proxy de confianza (raíz)
0
Impacto real (fuga/manipulación)
2
Escudos que aguantaron
todos
Proxies en los que se confiaba (comodín)
límite
La verdadera solución

Primero, divídelo: ¿"ataque" o "mi propio error"?

La regla es: no saltes a "¡ataque!" al ver errores. Si acabas de cambiar la configuración, sopesa "quizá mi cambio causó esto" con igual seriedad.

1

Sospecha de tu propio cambio

Hipótesis: un cambio reciente de configuración hizo que los usuarios re-iniciaran sesión repetidamente. Pero eso no puede explicar las secuencias de bytes malformadas en el valor almacenado → rechazado.
2

Deja que el valor sea la prueba

Lo que apareció fueron comillas con múltiple codificación URL y secuencias de bytes sobrelargas (redundantes) — no valores que produzcan usuarios o proxies reales → payload malicioso confirmado.
3

¿Dirigido o un escaneo?

Menos un golpe dirigido, y más probablemente un escáner automatizado barriendo todo internet de forma indiscriminada.

La visión de este sitio: al razonar hacia atrás desde los síntomas, mata las hipótesis con datos

"Lo que acabo de tocar parece sospechoso" es un buen punto de partida. Pero si la prueba (aquí, el propio valor) lo refuta, déjalo ir. Forma varias hipótesis y derríbalas una a una con datos, no con corazonadas — el diagnóstico erróneo cae en picado.

Qué fue el ataque: sondeos de inyección montados en un XFF suplantado

El bot empaqueta una cadena de sondeo en XFF "en lugar de una IP", con solo una IP de aspecto normal al final. El objetivo es uno solo —

"¿Esta app confía en el valor XFF y lo mezcla en bruto en una consulta de BD, o lo imprime directo en el HTML?"

Si la app es descuidada, la inyección SQL o el XSS aterriza vía XFF. Abusa de "el sitio que transporta una IP" como un punto de inyección de cadenas arbitrarias.

Por qué el impacto fue cero: dos escudos

Escudo ① valores enlazados (placeholders)

El valor de IP se pasó vía un placeholder como dato, no concatenado en una cadena. La entrada no puede cruzar a "comando", así que la SQLi no puede formarse por diseño — uno de los mayores beneficios de usar un framework.

Escudo ② validación del charset de la BD

Los bytes malformados (UTF-8 sobrelargo, etc.) son secuencias inválidas que el charset de la columna no puede almacenar. La BD rechazó la escritura y lanzó un error — el ataque falló y fue detectado.

Todo lo que ocurrió: las propias solicitudes del atacante dieron 500 y salió una avalancha de correos de excepción. Sin fuga, sin manipulación, sin inicio de sesión no autorizado. Un caso de manual de la defensa en profundidad funcionando como estaba previsto.

El problema real: confiar en todos los proxies

Si parásemos aquí sería una historia feliz — pero hay algo que no se puede ignorar. ¿Por qué llegó siquiera el XFF suplantado al manejo de IP?

✗ Confiar en todos los proxies (comodín)

XFF falso del cliente → adoptado como la "IP real" → registros, sesiones, comprobaciones de IP envenenados

✓ No confiar en nada (por defecto)

XFF falso ignorado → la IP real del socket se usa → la suplantación no tiene efecto

Con los proxies de confianza fijados a un comodín, el XFF suplantado de cualquiera pasa como la 'IP real'. Sin proxy delante, 'no confiar en nada' es lo correcto.

Este montaje no tenía proxy inverso ni CDN — el servidor web estaba directamente expuesto. Así que el XFF nunca necesitó confianza, y sin embargo estaba puesto en "confiar en todos". "Permitirlo todo sin más" es el atajo clásico de la fase de desarrollo que sobrevive hasta producción.

Punto ciego: la suplantación de XFF no es solo sobre inyección

Cualquier cosa que confíe en la IP del cliente queda envenenada: elusión del límite de tasa, elusión de la lista de IP permitidas, evasión de baneos, registros de auditoría falsificados. Incluso con defensas de inyección sólidas como rocas, esto es un problema aparte. Inventaria "¿dónde confío en la IP?" y comprueba la base de esa confianza (= tu configuración de proxy).

Solución: sanear en el límite → fijar la confianza correctamente

1

Sanea los encabezados de IP en el límite (parche, instantáneo)

Antes de que corra la lógica del proxy de confianza, divide el XFF, valida cada parte como "¿es esto una IP válida?", conserva solo las IP válidas; si ninguna lo es, descarta el encabezado entero. Las IP reales de los usuarios reales pasan, así que sin impacto; las cadenas de sondeo se descartan.
2

Fija los proxies de confianza correctamente (raíz)

Quita el comodín. ¿Sin proxy delante? → no confiar en nada. La confianza del proxy también afecta a la detección de http/https, así que los cambios pueden causar bucles de redirección — verifica primero en un entorno.
3

No confundas 'IP válida' con 'IP real'

Sanear es higiene. Una IP bien formada aún puede ser un valor falsificado. La confianza en la IP real viene solo de la configuración del proxy del paso 2.

Verifica: tras arreglarlo, repite el ataque para confirmar

Convierte "creo que lo arreglé" en "se detuvo con seguridad". ① aplica solo a un entorno → ② envíate tú mismo el mismo encabezado malformado → ③ confirma que los recuentos de errores no suben → ④ confirma que el acceso normal sigue funcionando → ⑤ despliega en todas partes. El despliegue por etapas previene accidentes.

Seguimiento: bloquea el encabezado de IP, te golpean en User-Agent (la trampa del juego del topo)

Decenas de minutos tras añadir el saneamiento, volvió el mismo tipo de error — en una columna distinta esta vez: user_agent, no ip_address. Bloqueado en el encabezado de IP, el atacante movió el mismo payload al encabezado User-Agent. Eso obliga a reencuadrar.

Entradas: XFF · User-Agent · Referer · … (infinitas)
↓ pero las que de verdad se almacenan son
Sumidero: solo ip_address y user_agent en la tabla de sesiones
↓ así que las entradas a sanear son
solo los encabezados que alimentan esas dos (encabezados de IP + User-Agent)
Matar 'qué encabezado es peligroso' uno a uno es un juego del topo. Razona hacia atrás desde 'dónde se almacena en última instancia la entrada no confiable (el sumidero)' y las entradas que sellar se vuelven finitas.

La pregunta correcta no es "¿qué encabezado es peligroso?" sino "¿a qué columna de BD aterriza en última instancia la entrada no confiable?" Si los únicos sumideros son ip_address y user_agent, sanear las entradas que los alimentan (encabezados de IP + User-Agent, más Referer por si acaso) detiene por diseño los errores derivados de las sesiones — otros encabezados no almacenados no pueden pivotarse. Los User-Agent legítimos son ASCII/UTF-8 válido, así que quitar solo los bytes inválidos no tiene impacto.

La visión de este sitio: razona hacia atrás desde el sumidero (no selles entradas una a una)

Sellar encabezados de uno en uno es un juego del topo — las entradas que un atacante puede usar son infinitas. Razona hacia atrás desde el sumidero (la columna almacenada / la salida) y enumera de golpe cada entrada que llega a él. Como nos enseñó "bloquea la IP, te golpean en User-Agent": cuando parchees un encabezado, sella al mismo tiempo las otras entradas que llegan al mismo sumidero. Y de nuevo — repetimos el segundo ataque y confirmamos que se detuvo antes de desplegar.

Y otra vez: el filtro de entrada fue eludido — defiende en la salida

Justo tras sellar también User-Agent, volvió el mismo error de la columna ip_address. El saneamiento de entrada estaba desplegado, y repetir el mismo encabezado malformado en una autoprueba no lo reproducía — y aun así producción seguía recurriendo. Este es el quid.

La razón: la lógica del framework para "resolver la IP del cliente" es internamente compleja — múltiples notaciones de encabezado (X-Forwarded-For, Forwarded, encabezados personalizados), combinaciones con los ajustes de confianza del proxy, orden de análisis y caché. En algún punto de esas rutas internas, un valor contaminado resurgió como la "IP del cliente" por una ruta que no habíamos saneado. El punto de entrada no puede atraparlo todo.

✗ Defender en la entrada

Sanear los encabezados de la solicitud → la IP del cliente resurge por otra ruta interna → eludido

✓ Defender en la salida

Comprobar justo antes de la escritura en BD → el único punto de estrangulamiento por el que todo pasa → sin elusión

Un filtro de entrada (comprobar los valores entrantes) puede ser eludido por las rutas internas de resolución. La salida (justo antes de la escritura en BD) es el único punto de estrangulamiento por el que todo pasa — nada se cuela.

Así que le dimos la vuelta y defendimos en la salida (justo antes de la escritura en BD). Subclasifica la clase de almacenamiento de sesiones y sobrescribe solo las partes que producen el valor (concepto): la IP almacenada se vuelve null salvo que sea una IP válida; al User-Agent almacenado se le quitan los bytes inválidos y se le limita la longitud. Por mucho que se comporte internamente la resolución de la IP del cliente, la comprobación siempre corre justo antes de la escritura, así que nada se cuela. Martilleando ambas rutas de ataque decenas de veces cada una, los recuentos de errores no subieron y las sesiones legítimas siguieron funcionando.

La visión de este sitio: la última línea de defensa es la salida (el punto de uso)

La máxima de seguridad es "no confíes en la entrada; valida/escapa siempre en el punto de uso (la salida)". Las comprobaciones de entrada ayudan a rechazar pronto, pero la información que el framework resuelve/deriva internamente (como la IP del cliente) puede colarse más allá de la entrada y resurgir. Validar/escapar justo antes de un sumidero peligroso — BD, HTML, un comando — es el punto de estrangulamiento por el que todo pasa, y esa es la última línea de defensa. Haz entrada y salida como dos cosas separadas.

La mayor lección: ser rechazado ruidosamente es más seguro

Aquí el ataque falló y se hizo visible como una avalancha de notificaciones de error. Irónicamente, eso no es malo: ser rechazado ruidosamente vence a ser dejado pasar en silencio. No descartes las notificaciones de error como "ruido" — úsalas como un sensor de anomalías. No te quedes en "bloqueamos el impacto"; arregla "por qué llegó a las partes internas". Esa es la postura de este sitio.

Sigue leyendo

FAQ

Q¿Puedo confiar en X-Forwarded-For?
A

No tal cual. XFF es un encabezado que el cliente puede fijar libremente — un atacante puede falsificar una IP con aspecto real o una cadena de sondeo de inyección. El único XFF fiable es el que añade un proxy que TÚ colocaste delante y en el que confías explícitamente.

Q¿Y si no uso un proxy inverso ni una CDN?
A

No confíes en ningún XFF en absoluto (deja el valor por defecto del framework). Si no hay proxy delante pero 'confías en todos los proxies', cualquiera puede alimentar a tu app con una IP falsa. Sin proxy en la ruta → confiar en nada es el ajuste correcto.

Q¿Basta con sanear el encabezado de IP?
A

Detiene los sondeos de inyección y la avalancha de errores, pero eso es higiene (frenar la hemorragia). Una 'IP con aspecto válido' no es una 'IP real'. La confianza en la IP real del cliente debe venir de la configuración del proxy de confianza, no del saneamiento.

QArreglé el encabezado de IP pero el mismo error volvió. ¿Por qué?
A

El mismo payload se movió a otro punto de entrada (p. ej. el encabezado User-Agent) y llegó a la misma columna almacenada. Parchear encabezados uno a uno es un juego del topo. Razona hacia atrás desde '¿a qué columna de BD / salida llega en última instancia la entrada no confiable?' y sanea juntas todas las entradas que la alimentan (encabezados de IP + User-Agent, etc.).