¿La inyección de solicitudes HTTP puede empezar con una línea nueva?
La inyección de solicitudes HTTP puede empezar con una línea nueva oculta. Aprende a rechazar CRLF en destinos y encabezados antes de renderizar solicitudes con credenciales.

Un inyector de credenciales puede mantener una clave de API fuera del contexto de un agente y aun así enviar esa clave en la solicitud equivocada. El fallo ocurre cuando la puerta de enlace trata la entrada del agente como texto inofensivo y luego la inserta en sintaxis HTTP después de que la capa de credenciales haya cumplido su función.
He visto equipos dedicar un gran esfuerzo al almacenamiento en bóvedas, los avisos de aprobación y los registros de auditoría, para después dejar un formateador de cadenas entre el agente y la red. Ese formateador pasa a ser parte del límite de seguridad. Si acepta un retorno de carro o una línea nueva en un destino, un nombre de encabezado personalizado o un valor de encabezado proporcionado por el agente, puede permitir que los datos creen estructura de solicitud.
La solución es deliberadamente sencilla: rechaza el retorno de carro y la línea nueva antes de renderizar la solicitud, analiza las entradas estructuradas en lugar de componerlas con cadenas y prueba los bytes que salen del proceso. No intentes sanear esto recortando o sustituyendo caracteres. Una solicitud rechazada es honesta. Una solicitud reparada puede ser una solicitud distinta.
¿La inyección de solicitudes HTTP puede empezar con una línea nueva?
Sí. En HTTP/1.1, una línea nueva separa partes del mensaje. Si el código coloca texto no confiable en una línea de solicitud o de encabezado sin imponer ese límite, un atacante puede usar CRLF, retorno de carro seguido de línea nueva, para terminar la línea actual y empezar otra.
Un renderizador vulnerable simplificado suele parecer inofensivo:
GET {path} HTTP/1.1\r\n
Host: {host}\r\n
Authorization: Bearer {vault_token}\r\n
{custom_name}: {agent_value}\r\n
\r\n
Supongamos que el agente proporciona este valor:
blue\r\nX-Forwarded-Host: internal.example
Los bytes renderizados ahora contienen un encabezado adicional. La credencial no se filtró al prompt del agente. El agente aun así influyó en la solicitud autenticada de una forma que el diseñador no pretendía.
El mismo tipo de error aparece en una cadena de destino cuando una puerta de enlace construye por sí misma la primera línea, o cuando una capa analiza una URL y otra combina más tarde una ruta, un host o un destino de proxy mediante formato de texto. El resultado exacto depende de la biblioteca HTTP y del comportamiento de los intermediarios. Esa incertidumbre no es una defensa. Cuando distintos componentes discrepan sobre los límites de un mensaje, una pequeña omisión de validación puede convertirse en un incidente de seguridad.
RFC 9110 define las líneas de campo HTTP como una sintaxis formada por un nombre de campo, dos puntos y un valor de campo. También trata las líneas nuevas como delimitación del mensaje, no como contenido ordinario dentro de un campo. Esta distinción importa más que una advertencia genérica de «escapar la entrada del usuario». No hay una forma escapada útil de una línea nueva dentro de un campo de encabezado para este fin. Recházala.
Una bóveda protege las credenciales, no la forma de la solicitud
La inyección de credenciales y la inyección de solicitudes resuelven problemas distintos. La primera mantiene el secreto lejos del agente. La segunda garantiza que la solicitud que recibe el secreto tenga la forma que aprobó una persona.
Piensa en una herramienta que permite a un agente llamar a una API de facturación con un token bearer almacenado en una bóveda y acepta un encabezado personalizado opcional. Un desarrollador podría considerar que el encabezado tiene poco riesgo porque el token nunca aparece en los argumentos de la herramienta. Ese razonamiento pasa por alto la proximidad. Una línea inyectada puede añadir un encabezado de reenvío, alterar una longitud de contenido, duplicar un encabezado de aplicación o contaminar una etiqueta de auditoría usada más adelante. La credencial sigue siendo secreta, mientras la acción se vuelve insegura.
Los destinos merecen la misma desconfianza. Un nombre de host que parece un dato puede convertirse en sintaxis de autoridad. Una ruta puede convertirse en un destino de solicitud. Un selector de proxy puede cambiar dónde se conecta el cliente. Si el agente puede influir en cualquiera de esos valores, la puerta de enlace debe decidir qué formas son válidas antes de adjuntar una credencial.
Por eso una lista de permitidos de nombres de host aprobados resulta útil, pero no basta. Limita a dónde puede apuntar una URL analizada. No demuestra que todos los componentes recibieran la misma URL analizada ni detiene líneas nuevas en la información de usuario, una anulación de encabezado o una cadena añadida después de comprobar la lista. El límite debe rechazar caracteres de control y conservar valores estructurados hasta el cliente HTTP.
Rechaza CR y LF antes de que exista cualquier objeto de solicitud
La regla más segura cabe en una frase: los campos de destino controlados por agentes, los nombres de encabezados personalizados y los valores de encabezados personalizados no deben contener ni \r ni \n después de decodificarlos.
Aplica esa regla antes de crear una URL, construir encabezados, elegir una opción de cliente o escribir una entrada de registro que más tarde alimente un reintento de solicitud. Comprueba la cadena original para detectar caracteres de control literales. Decodifica una vez según el contrato de entrada y vuelve a comprobar la cadena decodificada. No decodifiques repetidamente hasta que no cambie nada. La decodificación repetida convierte un contrato claro en conjeturas y puede producir resultados sorprendentes.
Una función de validación compacta tiene la forma adecuada cuando se niega a reparar la entrada:
validateRequestText(field, value):
if value contains "\r" or "\n":
fail(field + " contains a line break")
return value
En código real, valida el valor de cadena real del lenguaje, no una representación impresa. Una carga JSON puede contener "\n"; después de analizar el JSON, se convierte en un carácter de línea nueva. Comprobar los dos caracteres visibles, barra invertida y n, no detecta el valor peligroso.
No llames a trim() y continúes. En la mayoría de implementaciones, el recorte elimina solo los extremos, así que una línea nueva en medio sobrevive. Sustituir las líneas nuevas por espacios es peor de otra forma: cambia la acción solicitada y no deja al operador un registro claro de que el agente pidió sintaxis prohibida. Trata la entrada como no válida y detente antes de la E/S de red.
La respuesta de rechazo debe identificar el campo, pero evitar reflejar contenido sensible. custom header value contains a line break informa lo suficiente al llamador. Repetir el valor completo puede llevar credenciales o datos privados a una transcripción de terminal.
Los nombres de encabezados necesitan un contrato más estricto que los valores
Un valor de encabezado puede contener legítimamente espacios y muchos caracteres visibles. Un nombre de encabezado no. Permitir que un agente elija nombres arbitrarios crea más ambigüedad de la que la mayoría de puertas de enlace de acciones necesitan.
Usa una regla conservadora para los nombres: solo letras ASCII, dígitos y guion, con un límite de longitud razonable. Rechaza dos puntos, espacios en blanco, caracteres de control y bytes no ASCII, salvo que tengas una razón documentada para admitir una extensión concreta. El objetivo no es imitar todos los analizadores permisivos de internet. El objetivo es producir una sola solicitud sin ambigüedad.
Esta entrada debe fallar aunque su valor aparente sea ordinario:
{
"name": "X-Trace\r\nAuthorization",
"value": "debug"
}
También esta:
{
"name": "X-Trace: injected",
"value": "debug"
}
Los dos puntos pertenecen al renderizador, no al nombre proporcionado. Si la puerta de enlace los acepta, un analizador puede ver un nombre mientras otro ve una línea de campo completa. Un nombre de encabezado con espacio inicial invita a un desacuerdo parecido.
Los valores de encabezados necesitan su propia política además de CRLF. Decide si la puerta de enlace acepta encabezados repetidos, si permite comas y qué encabezados pueden anular los agentes. No combines nombres duplicados uniendo texto, salvo que la semántica del encabezado lo permita expresamente. Set-Cookie, Content-Length, Host, Authorization y los encabezados de reenvío merecen un tratamiento especial o una denegación total. Prefiero las puertas de enlace que reservan por completo los encabezados de protocolo y credenciales y ofrecen un pequeño conjunto de encabezados de aplicación que los agentes pueden proporcionar.
Esta recomendación a veces genera resistencia porque los encabezados personalizados arbitrarios hacen que una herramienta HTTP genérica parezca flexible. La flexibilidad es popular porque evita añadir una opción a la herramienta cuando una API quiere un encabezado más. Aun así, es el valor predeterminado equivocado para tráfico de agentes con credenciales. Una opción con nombre como idempotency_key o request_id tiene un formato claro y una persona responsable. Un conjunto de encabezados sin restricciones no tiene ninguno de los dos.
Analiza los destinos una vez y conserva sus partes estructuradas
Un destino deja de ser una cadena cuando la puerta de enlace lo acepta. Es un valor estructurado con esquema, host, puerto, ruta, consulta y, a veces, información de usuario o un fragmento. Analízalo una vez con un analizador de URL que conozca los estándares, rechaza los componentes no permitidos y entrega el valor analizado al cliente HTTP sin volver a convertirlo en una cadena de plantilla.
La lista de permitidos exacta depende de la acción. Una herramienta que llama a la API de un solo proveedor puede requerir HTTPS, un nombre de host y el puerto 443. Una herramienta más amplia puede permitir varios orígenes preconfigurados. En ambos casos, rechaza credenciales incrustadas, fragmentos, nombres de host malformados y cualquier retorno de carro o línea nueva decodificados antes de crear la solicitud. Los fragmentos no se transmiten por la red, pero aceptarlos añade confusión innecesaria a los registros y pantallas de aprobación.
Un límite de solicitud útil tiene este aspecto:
input destination
-> decode once
-> reject CR and LF
-> parse URL
-> require HTTPS
-> require allowed host and allowed port
-> reject user info and fragment
-> pass parsed URL to HTTP client
El orden es deliberado. Una lista de permitidos de hosts aplicada a una cadena sin procesar puede engañarse con información de usuario como https://[email protected]/. Una comprobación de líneas nuevas aplicada solo después de que una biblioteca haya normalizado un valor malformado puede no detectar lo que aceptó el analizador anterior. Analiza primero para entender el significado, pero rechaza los caracteres de control prohibidos antes de que cualquier componente pueda renderizarlos como sintaxis.
No construyas una solicitud HTTP concatenando scheme + "://" + host + path. Ese patrón vuelve a convertir un valor estructurado en texto justo cuando más necesitas las garantías del analizador. Si una biblioteca requiere argumentos separados de host y ruta, valida cada argumento con la misma regla de caracteres de control y usa la API estructurada de la biblioteca.
Las codificaciones crean evasiones cuando las capas discrepan
CRLF literal es la prueba que todo el mundo recuerda. CRLF codificado es la prueba que encuentra el error real.
Un agente puede enviar %0d%0a en un componente de URL. Si la puerta de enlace comprueba la cadena sin procesar y más tarde decodifica la URL antes de usar un renderizador personalizado, la comprobación ha protegido la representación equivocada. La línea nueva resultante aparece después de la validación. La doble codificación, como %250d%250a, cobra relevancia si distintas capas decodifican por separado.
Elige un límite de normalización explícito. Por ejemplo, analiza JSON una vez, decodifica por porcentaje solo donde lo exige el estándar de URL, valida la representación que consumirá el renderizador y prohíbe una decodificación genérica posterior. Conserva una prueba para cada transición. Es menos vistoso que un gran saneador, pero da a quienes revisan algo sobre lo que pueden razonar.
Una tabla de pruebas debe cubrir al menos estas entradas para cada campo controlado por el agente:
- un
\rliteral, un\nliteral y su forma CRLF adyacente %0d,%0ay%0d%0acuando se acepta la codificación por porcentaje%250d%250asi otro componente pudiera decodificar más tarde- un nombre de encabezado que contenga dos puntos o espacio en blanco
- un destino con información de usuario, un puerto inesperado o un host no aprobado
No pruebes solo si aparece un mensaje de error. Comprueba que el transporte cliente nunca se ejecute con una entrada rechazada. Un validador que informa de un error después de construir o poner en cola la solicitud ya ha incumplido la propiedad de seguridad que te importa.
Hay una trampa relacionada en los formatos de configuración. Las variables de entorno, YAML, JSON y los argumentos de shell tienen cada uno sus propias reglas de escape. Un elemento de prueba que visualmente contiene \n puede contener dos caracteres inofensivos, mientras que la decodificación JSON de producción lo convierte en una línea nueva. Crea las pruebas mediante la misma ruta de análisis que acepta llamadas reales de agentes.
El fallo suele esconderse en código de conveniencia
El código peligroso suele llegar después de la revisión de seguridad inicial. Alguien añade encabezados personalizados para una API nueva, hace configurable un proxy de depuración o escribe una función de reintento que reconstruye una solicitud para registrarla. Cada cambio parece razonable por separado. Juntos, pueden reintroducir el renderizado de cadenas después de que la puerta de enlace original hubiera usado API seguras del cliente.
He encontrado este problema siguiendo una acción desde la entrada hasta el socket, no buscando solo la palabra header. Busca interpolación de cadenas alrededor de Host:, Authorization:, Cookie:, Content-Length:, destinos de solicitudes, comandos de proxy, ayudantes de prueba HTTP sin procesar y utilidades de reproducción de registros. Busca también funciones de decodificación. Un validador seguro cerca del punto de entrada sirve de poco si una ruta posterior acepta una representación diferente.
Un fallo representativo tiene cuatro partes. La puerta de enlace valida un host de destino contra una lista de permitidos. Guarda el resto del destino como texto para un cliente de bajo nivel. Un ayudante de reintento decodifica por porcentaje ese texto para que los registros sean más fáciles de leer. El ayudante escribe entonces una línea de solicitud sin procesar para el reintento y %0d%0aX-Test:%20yes se ha convertido en sintaxis de mensaje.
Ninguna de esas líneas dice «permitir inyección de solicitudes». El fallo es la invariante ausente: el contenido no confiable nunca debe contener caracteres de salto de línea cuando el código renderiza un mensaje HTTP. Coloca esa invariante en un validador compartido y convierte el renderizado sin procesar en una excepción que las pruebas deban justificar.
Si usas una biblioteca HTTP de alto nivel, mantenla de alto nivel de principio a fin. Evita recurrir a un socket sin procesar para admitir un solo extremo extraño. Si una API exige de verdad un formato inusual en la red, da a esa integración una implementación específica y un modelo de entrada limitado. No debilites para ello la ruta genérica.
Los registros deben mostrar la intención rechazada sin repetir secretos
Cuando la validación rechace una solicitud, registra suficiente contexto para investigarla: hora, identidad de la sesión o proceso del agente, nombre de la acción, categoría del campo, motivo del rechazo y un resumen seguro o la longitud del valor proporcionado. No guardes por defecto el valor completo del encabezado. Los encabezados suelen contener tokens, datos personales o material firmado que se convierte en otro problema de gestión de secretos cuando se copia en registros.
Separa el evento de auditoría de una acción intentada del evento de una acción ejecutada. Una solicitud rechazada no debe parecer una llamada de red fallida. Nunca llegó a la red. Esta distinción ayuda en la respuesta a incidentes y evita que los operadores persigan un servicio remoto que nunca vio la solicitud.
Sallyport mantiene un diario de sesiones para las ejecuciones de agentes y un diario de actividad para las llamadas, ambos proyectados desde su registro de auditoría cifrado y encadenado con hash. Eso da a una puerta de enlace un lugar sensato para mostrar una acción rechazada, pero la regla de entrada sigue perteneciendo al momento anterior al renderizado HTTP.
La propiedad de verificación sin conexión importa aquí. sp audit verify puede verificar la cadena sobre texto cifrado sin la clave de bóveda, lo que puede establecer que los eventos registrados no se han alterado. No puede decirte si un validador era demasiado permisivo. El esquema de eventos y las pruebas deben hacer visible la categoría del campo rechazado sin conservar datos peligrosos.
Prueba la solicitud serializada, no solo el validador
Las pruebas unitarias de contains('\r') || contains('\n') son necesarias, pero insuficientes. Demuestran que una función rechaza dos caracteres. No demuestran que el transporte real no pueda recibir una línea nueva introducida por decodificación, valores predeterminados, reintentos o un adaptador personalizado.
Usa un servidor de pruebas local o un transporte de registro que capture el objeto de solicitud saliente y, cuando tu pila lo permita, sus bytes serializados. Introduce entradas aprobadas por el mismo punto de entrada de herramienta que usan los agentes. Comprueba el método, la autoridad, la ruta y el conjunto completo de encabezados. Después introduce casos rechazados y comprueba que el transporte de registro vio cero solicitudes.
Una matriz de pruebas compacta detecta más regresiones que una larga colección de pruebas de seguridad imprecisas:
field input expected result
custom header value "ok\r\nX-Added: yes" reject before transport
custom header name "X-Mode: injected" reject before transport
destination path "/v1/a%0d%0aX-Added:%20yes" reject after decoding
allowed destination "https://api.example/v1/a" one request, expected host
Añade pruebas de propiedades si el lenguaje las hace prácticas. Genera caracteres de control alrededor de cada clase de caracteres aceptada y comprueba el rechazo. Las pruebas de fuzzing ayudan solo después de que el contrato esté bien definido. Un fuzzer encontrará casos extraños del analizador, pero no puede decirte si los nombres de encabezados arbitrarios fueron una decisión de producto o un accidente.
Cuando falle una prueba, inspecciona los bytes finales antes de cambiar el validador. He visto equipos añadir más sustituciones hasta que un caso pasó, para descubrir después que el cliente había plegado espacios en blanco o decodificado dos veces. La reparación correcta suele ser eliminar el formateador o decodificador que hizo posible la sintaxis sin procesar.
La aprobación humana no excusa acciones malformadas
Una aprobación por sesión responde si un proceso de agente concreto puede actuar. Una aprobación por llamada responde si este uso de una credencial protegida merece una decisión humana. Ninguna hace segura una solicitud ambigua. Una persona no puede inspeccionar de forma fiable caracteres de control ocultos en un destino o valor de encabezado largo, y la fatiga de aprobación convertirá un aviso sutil en un trámite automático.
Mantén también estructurados los datos de aprobación. Muestra un host normalizado, método, ruta, categorías de encabezado con nombre y una indicación clara cuando la acción añade una credencial. Rechaza la entrada malformada antes de que aparezca la tarjeta de aprobación. Pedir a una persona que apruebe una solicitud no válida traslada un error del analizador a una interfaz de usuario.
La puerta de acceso a la bóveda, la autorización de sesión y la configuración de clave por llamada de Sallyport ofrecen momentos distintos para el control humano. El límite de la acción HTTP aún necesita un renderizador estricto porque la decisión de aprobación debe abarcar una acción bien formada, no una cadena cuyo significado cambia después del clic.
El primer cambio práctico no es una reescritura extensa. Encuentra cada ruta que acepte texto de destino o encabezados personalizados controlados por agentes, añade un rechazo incondicional de CRLF antes del renderizado y escribe una prueba de transporte que demuestre que la entrada rechazada no envía nada. Después elimina el código de conveniencia que reconstruye HTTP con cadenas. Ahí es donde este defecto sigue reapareciendo.
FAQ
¿Debo rechazar CRLF en los valores de encabezados HTTP?
Rechaza ambos caracteres en cualquier lugar donde puedan entrar en la sintaxis de una solicitud: cadenas de destino, nombres y valores de encabezados, anulaciones de método, fragmentos de cookies y configuraciones de proxy. Trata los datos decodificados por porcentaje de la misma forma. La validación debe ocurrir antes de la interpolación, el análisis o una llamada a una biblioteca que pueda normalizar la entrada.
¿Puede una biblioteca HTTP segura impedir el contrabando de solicitudes?
Una biblioteca cliente HTTP convencional ayuda, pero no elimina la necesidad de validar en el límite. Tu puerta de enlace puede decodificar, combinar o formatear la entrada del agente antes de que llegue a la biblioteca, y algunas entradas afectan primero a otro analizador. Rechazar saltos de línea en el límite deja explícito el contrato previsto.
¿Por qué son peligrosos los retornos de carro en las solicitudes de API?
No. Las líneas nuevas separan la sintaxis de los mensajes HTTP, por lo que permitirlas dentro de un destino o un campo de encabezado puede convertir datos en otro campo u otro componente de la solicitud. El código vulnerable aún puede enviar una solicitud sintácticamente válida, y por eso este error puede pasar desapercibido en pruebas normales.
¿CRLF codificado por porcentaje puede eludir la validación?
El analizador debe decodificar una vez y luego validar el valor decodificado. Si acepta un salto de línea codificado por porcentaje y una capa posterior lo decodifica, la comprobación ocurrió demasiado pronto. Mantén separados los valores de solicitud decodificados y renderizados para que el código posterior no vuelva a decodificarlos.
¿Cómo debo validar los nombres de encabezados personalizados?
Los nombres de encabezados deben usar una lista de permitidos pequeña, como letras, dígitos y guion, y rechazar cualquier otra cosa. Así se evitan saltos de línea, inserción de dos puntos, trucos con espacios en blanco y nombres que distintos componentes interpretan de forma diferente. No aceptes nombres de encabezados arbitrarios proporcionados por agentes solo porque se comprueba el valor.
¿Basta con analizar una URL para proteger un destino de agente?
No. Un analizador de URL puede indicar dónde están los límites del host y la ruta, pero no puede decidir si un destino está permitido. Analiza primero y después aplica tus propias reglas para el esquema, el host, el puerto, la información de usuario, los fragmentos y los caracteres de control decodificados.
¿Qué debe hacer una puerta de enlace de acciones cuando detecta una línea nueva?
Recházalo antes de crear cualquier objeto de solicitud. No lo recortes, sustituyas el salto de línea por un espacio ni lo elimines silenciosamente, porque esas correcciones ocultan la acción original en los registros y pueden cambiar el significado de una entrada firmada o autenticada. Devuelve un error que identifique el campo sin repetir un secreto.
¿La validación debe hacerse antes o después de decodificar la URL?
Comprueba la entrada sin procesar, luego la representación decodificada una sola vez y, en las pruebas, los bytes finales serializados. La primera comprobación detecta entradas claramente incorrectas; la comprobación decodificada detecta evasiones mediante codificación; la prueba de serialización detecta código de formateo inseguro. Una sola comprobación deja espacio para que una transformación posterior cambie el valor.
¿La inyección de credenciales puede causar inyección de solicitudes?
Aunque el código de confianza añada la credencial, el agente aún puede controlar la solicitud que la rodea. Un valor de encabezado hostil puede colocar un segundo encabezado junto a la credencial inyectada, mientras que un destino hostil puede redirigir el lugar al que va la solicitud con credenciales. Proteger el secreto y proteger la forma de la solicitud son tareas distintas.
¿Qué casos de prueba detectan la inyección de encabezados HTTP?
Prueba cada posición de entrada aceptada con retorno de carro literal, línea nueva literal, CRLF, formas codificadas por porcentaje, formas con doble codificación, espacios iniciales y dos puntos en un nombre de encabezado. Comprueba que la puerta de enlace rechace cada caso antes de la E/S de red. Después, inspecciona una solicitud capturada en los casos aprobados y verifica que contiene exactamente los campos previstos.