# Confusión del tipo de contenido en llamadas autenticadas de agentes

Las llamadas autenticadas de los agentes deben tener una única interpretación desde el borde de la red hasta el manejador de acciones. Si la pasarela ve una solicitud JSON inofensiva, la capa de autorización ve un conjunto de campos y el manejador ve un envío de formulario privilegiado, la credencial cumplió su función, pero la API falló.

No es un problema limitado a los antiguos formularios del navegador. Los agentes generan HTTP directamente, reintentan de forma agresiva, reutilizan ejemplos de las descripciones de herramientas y suelen operar con credenciales capaces de cambiar sistemas reales. El cuerpo de una solicitud forma parte de la decisión de autorización cuando selecciona un destino, una cantidad, un entorno, un comando o un permiso. Antes de decidir si el emisor puede actuar, debes hacer inequívocos su tipo de contenido, su sintaxis y su esquema.

## Una solicitud autenticada todavía debe significar una sola cosa

La autenticación responde quién presentó una credencial. La autorización responde si ese emisor puede realizar una acción. Ninguna de las dos respuestas indica si todos los componentes coincidieron en los argumentos de la acción.

Considera un endpoint que cambia el destino de un despliegue:

```http
POST /v1/deployments/promote HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json

{"environment":"staging","release":"2026.07.22"}
```

El código de autorización puede permitir la promoción a `staging`, pero denegarla en producción. Ese código solo es correcto si recibe el mismo valor de `environment` que usa el manejador de acciones. Si una capa de middleware lee JSON, un manejador consulta después los parámetros del formulario y ambos pueden rellenar un mismo objeto de solicitud, has creado dos fuentes de verdad.

El fallo no necesita un token criptográfico defectuoso. Un agente con una sesión legítima puede enviar un cuerpo que una capa ignore y otra acepte. Un agente comprometido puede hacer lo mismo. El resultado es una omisión de autorización expresada como formato de entrada.

RFC 9110 indica que `Content-Type` señala el tipo de contenido de la representación asociada y define tanto el formato de los datos como la manera en que el receptor debe procesarlos. Por eso, el encabezado forma parte de la semántica de la solicitud, no es un adorno. La misma RFC permite que un receptor sin `Content-Type` suponga `octet-stream` o inspeccione los datos. Eso puede ser útil para manejar archivos genéricos, pero es un mal valor predeterminado para APIs de acciones protegidas.

Para un endpoint de acciones, establece esta invariante:

> Exactamente un tipo de contenido aceptado convierte los bytes de la solicitud en exactamente un objeto de comando validado. Todas las decisiones de seguridad y todos los efectos secundarios usan ese objeto.

El endpoint puede admitir más de una representación, pero cada representación necesita su propio contrato y su propia batería de pruebas. No trates varios analizadores como simples comodidades intercambiables.

## Un encabezado Content-Type no es un esquema

`Content-Type: application/json` no significa «esta es la forma de solicitud que esperaba». Significa que el emisor afirma que el cuerpo usa un tipo de contenido JSON. Aún debes decidir si ese tipo es compatible con la ruta, si se permiten parámetros, si el cuerpo tiene una sintaxis válida y si el valor decodificado cumple el contrato de la operación.

Un endpoint protegido debe mantener deliberadamente pequeño el conjunto de representaciones permitidas. Muchos endpoints de comandos deberían aceptar solo JSON. Un endpoint de carga puede aceptar únicamente multipart. Una acción sin argumentos debería aceptar ningún cuerpo. Cuanto más amplio sea el conjunto aceptado, más rutas de análisis tendrás que mantener.

La guía REST Security Cheat Sheet de OWASP ofrece una recomendación práctica clara: documenta los tipos de contenido compatibles y rechaza los tipos inesperados o ausentes, aunque permite omitir el tipo de contenido cuando la solicitud tiene una longitud de cero. También advierte que el cuerpo y el tipo declarado deben coincidir para evitar interpretaciones distintas entre productor y consumidor.

Esa recomendación necesita una precisión para las acciones autenticadas. No «hagas coincidir» un tipo declarado inspeccionando el primer carácter y eligiendo un analizador. Que un cuerpo comience por `{` no autoriza a tratar como JSON una solicitud declarada como datos de formulario. La inspección convierte un contrato claro en una suposición de implementación.

Un contrato de ruta útil sería este:

| Ruta | Tipo de contenido permitido | Regla del cuerpo |
|---|---|---|
| `POST /v1/deployments/promote` | `application/json` | Objeto JSON obligatorio que cumple `PromoteRequest` |
| `POST /v1/artifacts` | `multipart/form-data` | Partes obligatorias que cumplen `ArtifactUpload` |
| `POST /v1/sessions/revoke` | ninguno | Debe contener cero bytes |

Sé preciso con los parámetros del tipo de contenido. Si tu analizador JSON acepta `application/json; charset=utf-8`, documéntalo y normaliza los parámetros mediante una sola biblioteca. Si solo acepta `application/json` sin parámetros, rechaza el parámetro en lugar de permitir que el proxy y la aplicación difieran. La elección importa menos que aplicarla una sola vez.

Separa también la preferencia de respuesta `Accept` del `Content-Type` de la solicitud. Un cliente puede pedir una respuesta JSON mientras envía un cuerpo inválido. Nunca dejes que un encabezado `Accept` amplíe los formatos de solicitud que analizará un endpoint de acciones.

## JSON necesita reglas más allá de una sintaxis válida

Un analizador JSON puede procesar correctamente una entrada que tu API debe rechazar. Los nombres de miembros duplicados son el ejemplo más evidente:

```json
{"environment":"staging","environment":"production","release":"2026.07.22"}
```

RFC 8259 dice que los nombres de objeto deberían ser únicos y explica por qué: los receptores no coinciden en el tratamiento de nombres duplicados. Muchos conservan el último valor, algunos fallan y otros exponen cada par. Es un problema documentado de interoperabilidad, no una preferencia de estilo teórica.

Supón que un middleware de autorización usa un analizador que conserva el primer valor de `environment`, mientras que un decodificador posterior conserva el último. El middleware aprueba staging y el manejador promociona producción. No puedes arreglarlo con mejores nombres de roles ni con otra afirmación del token. Rechaza la solicitud antes de que cualquiera de los dos componentes tome una decisión.

Haz lo mismo con los valores que parecen inofensivos en un lenguaje de tipos flexibles:

- Rechaza los miembros de objeto desconocidos en las solicitudes de acciones, salvo que exista una razón de compatibilidad documentada para conservarlos.
- Exige el tipo JSON esperado. Un booleano no es una cadena que casualmente dice `true`, y un identificador entero no es un número de coma flotante.
- Establece un tamaño máximo del cuerpo antes de analizarlo. Un validador de esquemas no puede proteger la memoria que ya agotaste al leer un cuerpo enorme.
- Decide si un campo puede omitirse, ser `null` o ser una cadena vacía. Son tres estados distintos.
- Rechaza los datos posteriores y las extensiones del analizador, como comentarios, `NaN` o nombres sin comillas, si tu biblioteca las ofrece.

No autorices directamente desde un mapa genérico. Decodifica en un tipo de solicitud con un esquema explícito, realiza la validación semántica y construye después un tipo de comando interno que no conserve artefactos del analizador. Un manejador que recibe `PromoteCommand { environment, release }` tiene menos margen para reinterpretar la entrada que uno que recibe un mapa, una colección de consultas, un objeto de solicitud y el cuerpo sin procesar.

Los números requieren especial cuidado. La gramática JSON permite literales numéricos grandes, pero muchos entornos convierten los números en una representación de coma flotante si no los configuras de otro modo. Si un valor identifica dinero, cuotas, registros de base de datos o un contenido firmado, usa un formato de cadena o un analizador de enteros con un rango documentado. No permitas que una capa redondee un número antes de que otra lo compare.

## Los cuerpos de formulario crean reglas ocultas para arrays y anidamiento

`application/x-www-form-urlencoded` parece sencillo porque se parece a una cadena de consulta. Deja de serlo cuando las bibliotecas empiezan a asignar significado a los nombres repetidos, la notación con corchetes, los signos más y los valores vacíos.

Considera estos cuerpos:

```text
role=user&role=admin
role[]=user&role[]=admin
role[user]=1&role[admin]=1
role=user%26role%3Dadmin
```

Distintos frameworks pueden tratarlos como un escalar final, un escalar inicial, un array, un objeto, nombres de campo literales o un error de análisis. Algunos middlewares analizan formularios para todos los métodos de solicitud. Algunos frameworks fusionan los parámetros de consulta y los parámetros de formulario en un objeto práctico. Es ahí donde las APIs protegidas pierden la pista de lo que realmente envió el emisor.

La guía de pruebas de OWASP sobre la contaminación de parámetros HTTP señala que el comportamiento depende de las interacciones entre la aplicación, el servidor web, el WAF y el middleware. Precisamente por eso hay que probar parámetros repetidos sin procesar, en lugar de confiar en la documentación del analizador de un único framework.

La recomendación popular de aceptar JSON y formularios codificados en URL en todos los endpoints «por compatibilidad con los clientes» suele ser equivocada. Se mantiene porque facilita escribir un cliente de demostración y porque muchos frameworks lo activan de forma predeterminada. También duplica los contratos de representación de cada acción y añade silenciosamente un tercer contrato cuando los campos de consulta se fusionan con el cuerpo.

Si debes admitir un endpoint de formulario, dale una política de análisis específica:

1. Rechaza los nombres repetidos, salvo que el esquema defina ese campo como una lista.
2. Rechaza la sintaxis con corchetes, salvo que el esquema defina su codificación exacta y el analizador la implemente de forma coherente.
3. Mantén separados los parámetros de consulta y los campos del formulario. No permitas que una fuente sobrescriba la otra.
4. Convierte los campos analizados en el mismo comando interno tipado que usa la ruta JSON, pero solo después de validarlos.
5. Prueba la codificación porcentual, `+` frente a `%20`, los valores vacíos, la ausencia de `=` y los campos duplicados a través de la ruta de solicitudes de producción.

No resuelvas esto eligiendo «gana el primero» o «gana el último». Eso produce una respuesta determinista dentro de un componente y conserva el desacuerdo en los demás. Un campo escalar protegido debe aparecer una sola vez.

## Multipart es un protocolo de carga, no un JSON flexible

`multipart/form-data` tiene una función legítima: transportar varias partes con encabezados independientes, a menudo con contenido de archivos. RFC 7578 lo define para valores de formularios y exige un parámetro boundary que separe las partes. Cada parte también puede incluir sus propios encabezados y metadatos de nombre de archivo.

Esa estructura convierte multipart en una mala representación alternativa para comandos autenticados normales. Tiene más sintaxis, más gestión de tamaños, más lugares donde pueden aparecer nombres de campo duplicados y más oportunidades para que una pasarela inspeccione una parte mientras la aplicación elige otra.

Un diseño problemático habitual acepta una parte JSON `metadata` junto a un archivo y también acepta campos de formulario de nivel superior que pueden sobrescribir los metadatos:

```text
Content-Disposition: form-data; name="metadata"

{"project":"alpha","visibility":"private"}

Content-Disposition: form-data; name="visibility"

public
```

Un componente puede autorizar basándose en `metadata.visibility`. Otro puede asociar la parte de formulario posterior con el parámetro `visibility` del manejador. La solicitud tiene dos valores para una propiedad sensible, expresados en dos gramáticas.

Diseña los endpoints multipart alrededor de partes con nombres y funciones distintos. Por ejemplo, acepta exactamente una parte `file` y exactamente una parte `manifest`. Exige que `manifest` sea JSON con su propio esquema estricto. Rechaza cualquier nombre de parte que no figure en el contrato de carga, rechaza las partes singleton duplicadas, establece límites independientes para el tamaño total del cuerpo y el tamaño del archivo, y decide si son necesarios los valores `Content-Type` de cada parte.

No confíes en un nombre de archivo como ruta, en una afirmación MIME como clasificación del archivo ni en el comportamiento de archivos temporales del analizador multipart como control de seguridad. Son problemas independientes de las cargas. La regla contra la confusión del analizador es más sencilla: las entradas de autorización deben proceder de una única fuente identificada y validada. Si `manifest.project` determina dónde se guarda un archivo, ninguna otra parte, parámetro de consulta o encabezado puede cambiar ese proyecto.

Cuando un comando no contiene archivos, no aceptes multipart. Cada tipo de contenido adicional es una forma más de que dos componentes discrepen.

## Un cuerpo vacío es un contrato, no la ausencia de validación

Algunas acciones autenticadas no necesitan argumentos. Revocar la sesión actual, rotar un nonce generado por el servidor o confirmar un evento fijo puede usar un cuerpo vacío. En esos casos, haz que el vacío sea obligatorio.

Un endpoint con contrato sin cuerpo debe rechazar todo lo siguiente:

```http
POST /v1/sessions/revoke HTTP/1.1
Content-Type: application/json
Content-Length: 2

{}
```

```http
POST /v1/sessions/revoke HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

scope=other
```

```http
POST /v1/sessions/revoke HTTP/1.1
Transfer-Encoding: chunked

0

```

El último ejemplo no contiene datos, pero sigue usando un mecanismo de encuadre que el contrato sin cuerpo quizá prohíba. Que lo rechaces depende de tu pila HTTP, pero debes decidirlo y probarlo en el borde. No permitas que un proxy transmita un encuadre que la aplicación interprete de otra manera.

En una ruta sin cuerpo, aplica estas reglas antes de la lógica de negocio:

- La solicitud no contiene bytes de contenido.
- La ruta no acepta `Content-Type`, salvo cuando una regla de compatibilidad lo permita explícitamente.
- La ruta no fusiona parámetros de consulta con el comando, salvo que cada nombre permitido aparezca en su propio esquema.
- El servidor registra la acción como una acción sin argumentos, en lugar de guardar un objeto de solicitud genérico que otros lectores puedan confundir después con una entrada.

RFC 9110 describe el contenido de la solicitud según la semántica del método y no concede un significado universal al cuerpo solo porque la solicitud use POST. Ese significado lo proporciona el contrato del recurso.

El caso incómodo es una biblioteca de cliente que siempre envía `{}`. No amplíes el endpoint solo para adaptarte a ella. Corrige el cliente o dale una ruta independiente y documentada. Un cuerpo que ahora no tiene efecto puede convertirse en un canal de entrada accidental después de un cambio posterior del manejador.

## Valida antes de autorizar y ejecuta el comando validado

La canalización más segura avanza en una sola dirección. Entran bytes sin procesar. La ruta selecciona un analizador permitido. El analizador produce un valor tipado. La validación produce un comando canónico. La autorización evalúa ese comando. El ejecutor recibe el mismo comando.

```text
raw HTTP request
  -> route and media-type check
  -> bounded body read
  -> one strict parser
  -> schema and semantic validation
  -> canonical command
  -> authorization
  -> execution and audit record
```

No inviertas las dos etapas centrales. La autorización suele necesitar campos como el ID del proyecto, el entorno, el destinatario o el modo del comando, así que los equipos se sienten tentados a inspeccionar pronto una entrada analizada de forma flexible. Eso crea un analizador previo a la autorización cuyo comportamiento debe ser idéntico al del decodificador final para siempre. Pocos sistemas mantienen esa promesa.

El comando canónico es un límite práctico, no un patrón para diagramas. Debe contener solo los valores que necesita el ejecutor y excluir el texto original del cuerpo, las colecciones de formularios, los objetos de solicitud del framework y los alias. Si el ejecutor recibe `target_environment`, no debe consultar después `req.query.environment` porque faltaba el destino o resultaba incómodo obtenerlo.

Esto también mejora los registros de auditoría. Registra el principal autenticado, el endpoint, el tipo de contenido aceptado, un resumen de la solicitud, los campos del comando canónico que sea seguro conservar, la decisión de autorización y el resultado. Registrar por defecto los cuerpos sin procesar crea otro problema, porque pueden contener credenciales, archivos subidos y datos de usuarios. Un resumen permite relacionar un evento con evidencias conservadas sin convertir los registros en un almacén de secretos.

La firma de solicitudes requiere la misma disciplina. Si el cliente firma bytes, pero el servidor autoriza un objeto normalizado, registra tanto las reglas de representación firmada como las de canonicalización. Si el cliente firma un objeto canónico, rechaza todas las codificaciones alternativas antes de verificar la firma. De lo contrario, dos secuencias de bytes pueden contener la misma solicitud de negocio, o una secuencia puede adquirir un significado distinto después del análisis.

## Prueba el desacuerdo, no solo el analizador correcto

Las pruebas unitarias que deserializan un único fixture JSON válido demuestran muy poco sobre la coincidencia entre analizadores. Tu objetivo de prueba es la ruta pública de solicitudes: balanceador o proxy inverso, pasarela, middleware del framework, manejador de ruta y cualquier servicio que vuelva a analizar el cuerpo.

Construye un corpus compacto de casos negativos para cada operación autenticada. Debe ejecutarse en CI contra un entorno desechable y comprobar tanto la respuesta como la ausencia de efectos secundarios. Una respuesta `400` no basta si ya se creó un mensaje en una cola, un evento de auditoría o una escritura parcial de archivo.

Empieza con este arnés de shell. Envía deliberadamente cuerpos sin procesar en lugar de depender de un cliente generado que rechace entradas mal formadas:

```bash
base=https://api.test.example
bearer='test-token'

send() {
  name=$1
  type=$2
  body=$3
  code=$(curl -sS -o "/tmp/${name}.out" -w '%{http_code}' \
    -X POST "$base/v1/deployments/promote" \
    -H "Authorization: Bearer $bearer" \
    -H "Content-Type: $type" \
    --data-binary "$body")
  printf '%-28s %s\n' "$name" "$code"
}

send valid_json 'application/json' \
  '{"environment":"staging","release":"2026.07.22"}'
send duplicate_json 'application/json' \
  '{"environment":"staging","environment":"production","release":"2026.07.22"}'
send form_body 'application/x-www-form-urlencoded' \
  'environment=production&release=2026.07.22'
send false_json 'application/json' \
  'environment=production&release=2026.07.22'
```

La forma esperada de la salida debe ser un éxito y tres rechazos del cliente:

```text
valid_json                   200
 duplicate_json               400
form_body                    415
false_json                   400
```

Tu convención de estados puede devolver `422` para una solicitud sintácticamente válida que no cumple el esquema. Conserva la distinción importante: un tipo de contenido incompatible nunca debe llegar a un analizador alternativo y un miembro JSON duplicado nunca debe llegar a la autorización.

Amplía el corpus con casos que apunten a los límites entre componentes:

| Caso | Qué debe ocurrir |
|---|---|
| Falta `Content-Type` con un cuerpo no vacío | Rechazar antes de analizar |
| Objeto JSON con un campo desconocido | Rechazar o aplicar un comportamiento de compatibilidad documentado |
| Campo escalar de formulario repetido | Rechazar |
| El valor de consulta entra en conflicto con el valor JSON | Rechazar o ignorar la consulta según el contrato de la ruta |
| Multipart contiene dos partes `manifest` | Rechazar |
| Una ruta sin cuerpo recibe `{}` | Rechazar |

Después, inspecciona el registro de auditoría. Cada entrada rechazada debe tener un rastro que identifique la ruta y la clase de rechazo sin registrar el contenido sensible de la solicitud. Cada entrada aceptada debe producir un único comando canónico. Si los registros muestran que la pasarela vio un destino y el manejador registró otro, has encontrado un desacuerdo aunque la prueba haya recibido una respuesta 2xx.

## Los proxies y middlewares también son analizadores

Los equipos suelen señalar el analizador de la aplicación y olvidar los componentes anteriores. Los proxies inversos pueden normalizar encabezados. Las pasarelas de API pueden inspeccionar JSON para aplicar una regla. Los WAF pueden analizar datos de formularios. El middleware de observabilidad puede leer y reconstruir un cuerpo. Un framework puede rellenar campos de consulta, formulario y JSON antes de que se ejecute el manejador de ruta.

La guía de OWASP sobre request smuggling describe la versión más amplia de este problema: los intermediarios y los servidores backend pueden interpretar de forma distinta los límites de una solicitud, especialmente durante la traducción de protocolos y el encuadre. La confusión del tipo de contenido no necesita request smuggling para ser peligrosa, pero ambos fallos surgen cuando distintas capas toman decisiones de análisis incompatibles.

Haz un inventario de cada lector de cuerpos en la ruta de acciones. Para cada uno, anota qué tipos de contenido analiza, si conserva valores duplicados, si descomprime contenido, si impone un límite de tamaño y si puede reescribir el cuerpo. Si nadie puede responder esas preguntas, el endpoint todavía no está listo para credenciales de agentes.

Mantén limitado el papel de la pasarela. Puede aplicar límites de cuerpo por ruta y bloquear tipos de contenido que una ruta nunca acepta. También puede rechazar encabezados mal formados antes de que lleguen a la aplicación. Pero no uses una transformación de la pasarela para convertir datos de formulario en JSON ni para «limpiar» campos duplicados. La aplicación debe rechazar la ambigüedad usando exactamente la semántica que ejecutará.

Prueba las versiones HTTP y las rutas de despliegue que realmente usa producción. Una solicitud que funciona correctamente contra un servidor local de desarrollo puede cambiar cuando un cliente HTTP/2 llega a un proxy que reenvía HTTP/1.1 a la aplicación. El objetivo no es construir un laboratorio de investigación de ataques. Es demostrar que la cadena de producción produce un único objeto de comando por cada solicitud aceptada.

## Las pasarelas de agentes deben conservar el límite

Una pasarela de agentes debe mantener las credenciales alejadas del modelo y conservar un registro de la acción, pero no puede hacer segura por sí sola una API de destino permisiva. La pasarela debe enviar una representación que la ruta de destino admita explícitamente, y el destino debe validar esa representación antes de evaluar la autoridad.

El canal HTTP de Sallyport inyecta las credenciales y mantiene las claves de API fuera del agente, de modo que el agente puede solicitar una acción sin recibir el secreto. Es un límite de credenciales útil. Combínalo con contratos de endpoint que rechacen cuerpos ambiguos, porque las credenciales protegidas siguen autorizando la solicitud que llega a la API.

Proporciona a los agentes herramientas que reflejen el contrato en lugar de exponer una acción genérica de «hacer cualquier solicitud HTTP» para sistemas sensibles. Una herramienta de promoción debe aceptar argumentos tipados `environment` y `release`. Su implementación debe serializar un único objeto JSON, establecer un solo tipo de contenido y rechazar las entradas de la herramienta que no cumplan el esquema de la API. El servicio receptor debe repetir la validación. Los esquemas de herramientas reducen errores, pero no sustituyen la desconfianza del servidor.

Cuando un agente necesite cargar un archivo, conviértelo en una herramienta independiente con un archivo identificado y un manifiesto identificado. Cuando necesite una acción sin argumentos, no le proporciones ningún campo de cuerpo. Estas pequeñas restricciones facilitan inspeccionar, aprobar, reproducir en un entorno de pruebas y auditar después la solicitud prevista por el agente.

No apruebes una capacidad vaga esperando que los analizadores aporten la precisión que falta. Haz que el endpoint acepte un único significado, que el agente envíe ese significado y que rechace cualquier forma alternativa antes de que una credencial pueda autorizarlo.
