Guías de seguridad

Qué es la suplantación de X-Forwarded-For (XFF): mala configuración del proxy de confianza y cómo defenderse

El cliente puede falsificar X-Forwarded-For (XFF), y confiar en todos los proxies deja pasar el valor falso. Cómo sanear el encabezado y corregir el ajuste.

Publicado 2026-06-08 13 min de lectura
En esta página

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
Protecciones que funcionaron
① placeholders (valores enlazados) ② validación del charset de la BD
La mala configuración que quedó
"confiar en todos los proxies" — sin proxy realmente delante
Respuesta
Sanear los encabezados de IP en el límite (corto plazo) → arreglar la config del proxy de confianza (causa de fondo)
0
Impacto real (fuga/manipulación)
2
Protecciones que funcionaron
todos
Proxies en los que se confiaba (comodín)
límite
La solución principal

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

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 partir de los síntomas, contrasta cada hipótesis con los 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 descártalas una a una con datos, no con corazonadas; así te equivocarás de causa mucho menos.

Qué fue el ataque: un XFF suplantado que llevaba sondeos de inyección

El bot mete una cadena de sondeo en XFF "en lugar de una IP", con solo una IP de aspecto normal al final. Comprueba una sola cosa:

"¿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 tiene éxito vía XFF. Abusa de "el encabezado que transporta una IP" como un sitio por el que enviar cadenas arbitrarias.

Por qué no hubo daño: dos protecciones

Protección ① valores enlazados (placeholders)

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

Protección ② 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í no habría ningún problema, 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" → los registros, las sesiones y las comprobaciones de IP reciben el valor falso

✓ 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 un ajuste habitual de la fase de desarrollo que se queda en producción.

Fácil de pasar por alto: la suplantación de XFF no es solo sobre inyección

Cualquier cosa que confíe en la IP del cliente acaba usando el valor falso: 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, 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 (corto plazo, inmediato)

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 (causa de fondo)

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 solo corrige el formato. 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: tras bloquear el encabezado de IP, el ataque pasó a User-Agent

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 exigía otro enfoque.

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)
Bloquear encabezados peligrosos uno a uno solo traslada el ataque a otro encabezado. Parte de dónde se almacena en última instancia la entrada no confiable (el sumidero), y las entradas que debes tratar se convierten en una lista corta y fija.

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; mover el payload a otros encabezados no almacenados no provoca ningún error. 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: parte del sumidero en lugar de bloquear entradas una a una

Bloquear encabezados de uno en uno no funciona: un atacante siempre puede usar otro encabezado. Razona hacia atrás desde el sumidero (la columna almacenada / la salida) y enumera de golpe cada entrada que llega a él. Como mostró este caso cuando el ataque pasó del encabezado de IP a User-Agent: cuando parchees un encabezado, trata 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: defender justo antes de guardar el valor

Justo tras tratar 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í el error seguía repitiéndose en producción. Este es el punto clave del caso.

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 malicioso volvió como la "IP del cliente" por una ruta que no habíamos saneado. Comprobar en el punto de entrada no puede atraparlo todo.

✗ Defender en la entrada

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

✓ Defender en la salida

Comprobar justo antes de la escritura en BD → el único sitio por el que pasa cada valor → 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 sitio por el que pasa cada valor, así que nada se le escapa.

Así que cambiamos de enfoque 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 le escapa. Enviando el encabezado malicioso por 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 comprobación final va en la salida (el punto de uso)

La regla básica 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 pasar más allá de la comprobación de entrada y volver a aparecer. Validar/escapar justo antes de un sumidero peligroso (BD, HTML, un comando) es el único sitio por el que pasa cada valor, así que ahí va la comprobación final. Haz entrada y salida como dos cosas separadas.

Lección: es más seguro que la entrada maliciosa se rechace con un error visible

Aquí el ataque falló y se hizo visible como una avalancha de notificaciones de error. Eso no es un mal resultado: ser rechazado con un error visible es más seguro que pasar sin que nadie lo note. No descartes las notificaciones de error como ruido; úsalas para detectar 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 pasarle a tu app 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 es solo una solución a corto plazo. 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 encabezado (p. ej. el encabezado User-Agent) y llegó a la misma columna almacenada. Si parcheas encabezados uno a uno, el atacante simplemente pasa a otro. 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.).