CRLF — son solo dos caracteres de salto de línea, pero en HTTP juegan un papel clave: separan las cabeceras del cuerpo y separan las líneas de cabeceras. Cuando estos caracteres aparecen en lugares inesperados —por ejemplo, en la ruta URL o en los parámetros de consulta— y luego se “arrastran” a las cabeceras HTTP, sucede lo típico: suplantación de respuestas, envenenamiento de caché, basura en los registros y otras molestias. En este artículo analizaremos por qué ocurren estos errores, cómo los provocan codificaciones “no estándar” y decodificadores exóticos, qué consecuencias se encuentran con más frecuencia y qué deben hacer desarrolladores y especialistas en seguridad para estar tranquilos.
Por qué CRLF es importante
En HTTP/1.1 el separador de líneas de las cabeceras es la secuencia CRLF (Carriage Return + Line Feed). Esto está definido en las especificaciones HTTP y significa que la presencia de estos caracteres en los datos con los que se forman las cabeceras cambia la estructura de la respuesta por completo: tras un CRLF comienza una nueva línea de cabecera, y tras una línea vacía comienza el cuerpo de la respuesta. Es decir, una sola entrada “inocente” de caracteres de control puede convertir un fragmento de entrada de usuario en una cabecera separada o incluso “arrancar” parte de la respuesta. Más detalles sobre los formatos de los mensajes y los separadores de línea están en RFC 9112 (HTTP/1.1) y RFC 9110 (HTTP Semantics).
Dónde aparece CRLF "por sí solo"
- El servidor o un componente intermedio concatena la entrada del usuario con una cabecera (por ejemplo, construye
Locationa partir del parámetronext). - El servidor web o el proxy inverso repasan datos desde la URL a las cabeceras (por ejemplo, a una cabecera personalizada
X-Forwarded-*o a los registros). - El framework permite al desarrollador formar cabeceras manualmente en el controlador y allí entra entrada no saneada.
De dónde proviene la inyección: ruta y query
En la práctica, los escenarios más comunes son aquellos en los que la aplicación acepta la ruta/los parámetros y reenvía al usuario a una nueva dirección (redirección), genera un archivo con el nombre indicado (cabecera Content-Disposition), proxya parte de la solicitud a otro servicio o escribe información en registros/métricas. Si entre la “entrada del usuario” y la “cabecera HTTP” no existe un límite firme, la inyección CRLF se convierte en real.
Ejemplo: redirección insegura
Enfoque problemático. El controlador recibe GET /redirect?next=/cabinet y luego hace: res.set('Location', next). Si en next aparecen caracteres de control, se pueden “añadir” nuevas cabeceras o cambiar el cuerpo de la respuesta.
Enfoque seguro. Usar la función integrada del framework para redirecciones, que: (a) codifica estrictamente la URL, (b) no permite insertar caracteres de control, (c) aplica canonización. Si por requisitos hay que soportar URLs relativas, aceptar solo una lista blanca de rutas permitidas.
Codificaciones no estándar y decodificadores "mágicos"
Cuando se habla de “CRLF en parámetros”, muchos imaginan la aparición directa de los caracteres de salto de línea. Pero el verdadero caos comienza por las capas de decodificación y las particularidades históricas de las codificaciones. Algunos componentes hacen percent-decode, otros realizan un segundo decode “por si acaso”, otros soportan formas antiguas de representación de caracteres (por ejemplo, %uXXXX en herencias de algunas plataformas), y otros intentan “reconstruir” secuencias inválidas y obtienen caracteres de control.
Las reglas clásicas para construir URLs están en RFC 3986 (URI) y RFC 3987 (IRI), pero muchas capas del stack interpretan las secuencias “sucias” de forma distinta, y eso abre la puerta a inyecciones en casos límite.
Qué puede salir mal
- Doble decodificación. Un proxy hace percent-decode y luego la aplicación lo hace de nuevo. En la entrada hay caracteres inofensivos, en la salida aparecen caracteres de control.
- Formas de codificación mixtas. Se encuentran representaciones heredadas como
%u000d/%u000au otras formas no estándar que algunos decodificadores aceptan por costumbre. - Procesamiento de UTF-8 no válido. Algunas bibliotecas intentan “arreglar” el flujo de bytes y recuperan un carácter de salto de línea donde no se esperaba.
- Normalización Unicode. Raro, pero posible: la normalización a NFC/NFD cambia secuencias de bytes y luego otra capa de decodificación interpreta el flujo de forma distinta.
- Parseadores tolerantes. Históricamente, algunos servidores aceptaban un simple LF en las cabeceras. Las especificaciones modernas son más estrictas, pero el parque real de software es heterogéneo, especialmente en los límites (servicios legacy, proxies antiguos).
Un buen punto de referencia sobre las clases de ataques a cabeceras está en los materiales de OWASP sobre CRLF/HTTP response splitting: OWASP: CRLF Injection y OWASP: HTTP Response Splitting.
Consecuencias típicas
La inyección CRLF no es una única vulnerabilidad, sino todo un conjunto de efectos que dependen del contexto.
- Suplantación/inyección de cabeceras. Desde
Set-CookieyLocationhastaContent-Type, lo que permite alterar el comportamiento del navegador y de las cachés. - HTTP Response Splitting. División de la respuesta en dos: la primera termina “antes de tiempo” y la segunda comienza con cabeceras y cuerpo arbitrarios (a menudo se usa para envenenar cachés). El fenómeno está tratado en trabajos clásicos sobre seguridad de aplicaciones web; véase el repaso de OWASP citado más arriba.
- Envenenamiento de caché. Suplantación de cabeceras de caché o del estatus de la respuesta para “fijar” contenido malicioso en CDN/proxy y servirlo a otros usuarios. Aspectos prácticos están descritos, por ejemplo, en el material de PortSwigger sobre cache-poisoning (véase su documentación: PortSwigger: Web cache poisoning).
- Inyecciones en registros/analítica. Los caracteres de control “rompen” el formato de los registros, ocultan solicitudes reales o insertan líneas falsas, lo que complica las investigaciones.
Antipatrones en el código
A continuación hay dos ejemplos agregados. No están ligados a un framework concreto y muestran la idea general: no se puede concatenar directamente la entrada del usuario con las cabeceras.
Inseguro: formar Location a partir de un parámetro
// Pseudo-JS
const next = req.query.next; // <-- entrada del usuario
res.setHeader('Location', next); // <-- inserción directa en la cabecera
res.statusCode = 302;
res.end();
Más seguro: verificación, canonicalización, codificación
// Pseudo-JS
const next = String(req.query.next || '/');
if (!next.startsWith('/')) { return res.redirect(302, '/'); } // solo rutas relativas
if (/[r
]/.test(next)) { return res.redirect(302, '/'); } // prohibición estricta de caracteres de control
res.redirect(302, next); // usar la API del framework, que gestiona las cabeceras correctamente
Inseguro: nombre del archivo en Content-Disposition
# Pseudo-Python
filename = request.args.get('name', 'report.txt')
response.headers['Content-Disposition'] = f'attachment; filename="{filename}"' # peligroso
Más seguro: codificación compatible con RFC para parámetros
# Pseudo-Python
import re, urllib.parse
filename = request.args.get('name', 'report.txt')
if re.search(r'[r
]', filename): filename = 'report.txt'
# Usamos filename* con codificación compatible con RFC 5987 / 6266
disposition = "attachment; filename*=UTF-8''" + urllib.parse.quote(filename, safe='')
response.headers['Content-Disposition'] = disposition
Las recomendaciones sobre la correcta transmisión de nombres de archivo en cabeceras están en RFC 6266.
Cómo probar (de forma segura y legal)
Pruebe únicamente en sus entornos de pruebas o con autorización expresa del propietario del sistema. El objetivo es confirmar el tratamiento correcto de los caracteres “problemáticos” y la ausencia de doble decodificación.
- Conjunto de caracteres. Pase entradas que contengan caracteres de control en distintas representaciones (literalmente, en percent-encoding, en formas “exóticas”), pero hágalo exclusivamente en un entorno de pruebas.
- Capas. Proxy → servidor web → aplicación: compruebe si se produce decodificación en cada paso.
- Herramientas. Son útiles las solicitudes manuales (
curl) y el repetidor/decodificador de Burp Suite. Para un enfoque sistemático conviene el OWASP Web Security Testing Guide.
Prácticas de protección
La idea principal es una “frontera estricta” entre la entrada del usuario y las cabeceras HTTP. Todo lo que llegue a una cabecera debe estar en una lista blanca o estar correctamente codificado para el contexto específico.
- No concatenes cabeceras manualmente. Usa la API del framework (
redirect(),set_cookie(),send_file(), etc.) que valida los valores por ti. - Prohíbe caracteres de control. Un filtro simple
[ren todas las rutas que vayan a cabeceras (nombres de archivos, rutas de redirección, nombres de métricas).
] - Canonización → validación → codificación. Primero lleva la cadena a una forma canónica, luego valida (listas blancas) y finalmente codifica para el contexto objetivo (por ejemplo,
filename*). - Un solo decode en el borde. Evita la “desempaquetación” repetida de la entrada en distintas capas. Fija de forma explícita dónde ocurre la decodificación de la URL.
- Proxy y servidores web. Activa un tratamiento estricto de cabeceras, descarta las no válidas y limita longitudes. Sigue las recomendaciones de las especificaciones HTTP sobre el formato de los mensajes ( RFC 9112).
- Registros. Escapa los caracteres de control antes de escribirlos, o registra en un formato estructurado (JSON Lines) con escape explícito.
- Cache/CDN. Activa un modo “estricto” para las cachés y establece
Vary/Cache-Controldesde la aplicación para reducir el riesgo de combinaciones inesperadas de cabeceras.
Lista de verificación de auditoría
- Buscar lugares donde la entrada del usuario llegue a cualquier cabecera (redirecciones, adjuntos, reenvíos por proxy, métricas).
- Comprobar que la cadena se valida para ausencia de
r/y que no puede llegar a la cabecera “tal cual”. - Asegurarse de que se usa la API del framework y no la construcción manual de cadenas.
- Fijar el punto único de decodificación de URL y prohibir la decodificación “oculta” repetida en capas intermedias.
- Probar en el entorno de pruebas casos límite típicos: valores largos, codificaciones inusuales, secuencias “sucias”.
- Comprobar la configuración del servidor web/proxy: descarte de cabeceras no válidas, límites en tamaño/cantidad, modelo de parseo estricto.
Preguntas frecuentes y matices
¿Es suficiente filtrar solo '
'?
No. Hay que filtrar ambos caracteres de control: tanto r como . En stacks reales hay parseadores tolerantes a un simple LF, por lo que es más fiable prohibir cualquier carácter de control (incluyendo otros códigos de control) en los datos que vayan a cabeceras.
¿Y si realmente necesito pasar caracteres "especiales"?
Codifícalos según el contexto. Para URL — percent-encoding estándar; para nombres de archivo — el parámetro filename* con codificación UTF-8 (véase RFC 6266); para cuerpos de solicitud/respuesta — usa el Content-Type adecuado y el escape dentro del formato.
¿Puede el problema manifestarse solo en producción y no en dev/stage?
Sí. A menudo la causa son las diferencias en la cadena: localmente se accede directamente a la aplicación, pero en producción existe una cadena CDN → WAF → proxy inverso → balanceador → aplicación. Cualquiera de estas capas puede añadir o quitar decodificaciones y cambiar la interpretación de la entrada.
Herramientas y enlaces útiles
- RFC 9112: HTTP/1.1 — especificación del formato de los mensajes y de los separadores de línea.
- RFC 9110: HTTP Semantics — semántica de HTTP y de las cabeceras.
- RFC 3986: URI y RFC 3987: IRI — cómo se codifican URL/IRI.
- OWASP: CRLF Injection y OWASP: HTTP Response Splitting — repaso de la problemática y los riesgos.
- PortSwigger: Web cache poisoning — aspectos prácticos del envenenamiento de caché.
Conclusiones
Las inyecciones CRLF a través de query y path siempre van sobre fronteras y contextos. En cuanto la entrada del usuario se filtra en las cabeceras sin reglas estrictas, se juega a la lotería con el stack: alguien decodifica “de más”, alguien considera válida una secuencia extraña —y entonces las cabeceras “se descontrolan”. La solución es disciplina: canonización, validación, codificación contextual y evitar la construcción manual de cabeceras. Así, cualquier codificación “no estándar” seguirá siendo simplemente bytes y no una puerta hacia su respuesta y su caché.