TL;DR: La entrada de tu API es una superficie de ataque, así que pruébala como tal. Escribe casos negativos que envíen campos sobredimensionados, tipos incorrectos, cuerpos mal formados y cadenas de inyección, y verifica que el endpoint responda con un 4xx y nunca con un 5xx. Convierte la validación de esquema en un control de seguridad con
additionalProperties: false, enums y límites de longitud. Ejecuta toda la suite en CI con cada cambio. Los agentes de IA hacen esto urgente: generan y reenvían cargas útiles a la velocidad de la máquina, por lo que «cargar estos datos» que se convierte silenciosamente en «ejecutar este código» ahora escala.
La mayoría de las suites de pruebas demuestran que tu API funciona cuando quien llama se comporta correctamente: envías un cuerpo válido, recibes un 200 y la aserción pasa. Pero eso dice poco sobre lo que ocurre cuando el cuerpo es hostil. Considera no confiable cualquier dato que tu endpoint no haya generado: cuerpos de solicitud, parámetros de consulta, encabezados, archivos, cargas de webhooks y el JSON que un agente de IA construye dinámicamente. Supón que alguien terminará enviando la peor variante posible.
En julio de 2026, Hugging Face describió un incidente de seguridad cuyo vector de entrada eran datos, no una contraseña robada. Cubrimos las lecciones de esa brecha por separado; esta guía se centra en la implementación. Construirás pruebas que envían entradas similares a las de un atacante y las ejecutarás automáticamente con cada cambio. Las categorías se alinean con el Top 10 de Seguridad de API de OWASP. Apidog sirve para diseñar el contrato y ejecutar estas pruebas, pero los patrones funcionan con cualquier framework.
La entrada es una superficie de ataque, no un campo de formulario
La validación suele tratarse como una mejora de UX: detectar un correo vacío, mostrar un borde rojo y continuar. En una API, es un control de seguridad.
Cada campo aceptado es una promesa que el cliente puede romper:
- Un
limitesperado como entero pequeño llega como999999999. - Un
filenameesperado como una palabra llega como../../etc/passwd. - Un objeto
configesperado como configuración llega como un conjunto de instrucciones. - Un campo de texto puede contener una expresión de plantilla, SQL o comandos de shell.
Las pruebas de seguridad no son una disciplina aislada que se añade al final. Son pruebas negativas enfocadas en los campos que pueden causar más daño. Empieza con esta pregunta para cada input:
¿Cuál es la peor entrada que cabe en este campo?
Esta pregunta es la base de las mejores prácticas de seguridad de API. El resto del artículo la convierte en pruebas reproducibles.
Cómo «cargar estos datos» se convirtió en «ejecutar este código»
El incidente de Hugging Face muestra por qué los datos de entrada necesitan este nivel de atención. La compañía indicó que el vector de entrada fueron conjuntos de datos maliciosos: un dataset manipulado activó un cargador de conjuntos de datos de código remoto, y una inyección de plantilla estaba incluida en una configuración de dataset. Consulta el relato de la compañía en su informe de incidentes de seguridad.
La forma del fallo es importante:
- Un endpoint acepta algo descrito como datos.
- Cargar esos datos ejecuta una ruta de código.
- Esa ruta interpreta instrucciones controladas por quien envió la carga.
En otras palabras, «cargar estos datos» se convierte en «ejecutar este código».
La inyección de plantilla sigue el mismo patrón: un valor que debía ser texto inerte termina evaluándose. Por eso, cualquier endpoint que acepte un nombre de cargador, formato, plantilla, objeto serializado o blob de configuración puede estar aceptando instrucciones, incluso si no fue diseñado con ese propósito.
Si nunca has enviado una configuración hostil a ese endpoint durante las pruebas, no has verificado que permanezca inerte.
Validación de esquema como control de seguridad
El control más barato que puedes aplicar es un esquema estricto en el borde de la API. Un esquema no es solo documentación: cuando rechaza solicitudes que no coinciden con el contrato, se convierte en un filtro que se ejecuta antes de que la lógica de negocio procese los datos.
JSON Schema ofrece primitivas para definir ese filtro:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["loader", "name"],
"properties": {
"loader": {
"enum": ["csv", "json", "parquet"]
},
"name": {
"type": "string",
"maxLength": 128,
"pattern": "^[\\w .-]+$"
},
"rows": {
"type": "integer",
"minimum": 0,
"maximum": 1000000
}
}
}
Este esquema aplica varias defensas concretas:
| Regla | Qué evita |
|---|---|
additionalProperties: false |
Campos inesperados, como un template contrabandeado. |
enum en loader
|
Valores como pickle:// o cargadores no admitidos. |
maxLength |
Cadenas de varios megabytes que agotan recursos. |
pattern en name
|
Caracteres y formatos no permitidos, incluidos patrones como {{. |
minimum y maximum
|
Valores numéricos fuera del rango operativo esperado. |
La seguridad no está en que estas reglas «conozcan» los ataques. Está en que aceptan únicamente el conjunto reducido de valores que tu API realmente admite.
Un esquema no detiene todos los exploits. Sí reduce una categoría común de fallos: «nunca comprobamos lo que este endpoint acepta».
Pruebas negativas: demuestra que el endpoint dice que no
Las pruebas de ruta feliz verifican que una entrada válida produce una salida válida. Las pruebas negativas verifican que una entrada inválida recibe un rechazo controlado.
Para cada campo, crea casos para:
- Tipo incorrecto.
- Campo ausente cuando es obligatorio.
- Campo presente cuando está prohibido.
- Valor demasiado largo.
- Valor fuera de rango.
- Formato inválido.
- Cadenas de inyección relevantes para ese campo.
Después, verifica dos condiciones en cada respuesta:
- El estado es
4xx, normalmente400,413o422. - El estado nunca es
5xx.
Un 400 indica que el límite funcionó. Un 500 indica que la entrada hostil alcanzó código que no estaba preparado para manejarla.
La lista de verificación de pruebas de seguridad de API incluye una base que puedes adaptar campo por campo.
Evita afirmar mensajes de error exactos. Este tipo de prueba es frágil:
assert response.json()["error"] == "cargador inválido"
Prefiere comprobar comportamiento y ausencia de efectos secundarios:
assert response.status_code in (400, 413, 422)
assert response.status_code < 500
Cuando sea posible, verifica también que no se creó ningún recurso, no se modificaron datos y no se ejecutó ningún trabajo asíncrono.
Clases de inyección que merecen una prueba dedicada
No necesitas cubrir todas las variantes posibles de cada ataque. Necesitas al menos un caso permanente por clase para detectar regresiones de forma visible.
Las herramientas de detección automatizada de vulnerabilidades de API pueden ampliar la cobertura, pero estos casos manuales detectan primero los errores obvios.
Inyección SQL
Envía una carga como esta a cualquier campo que llegue a una consulta:
1); DROP TABLE datasets;--
El endpoint debe tratarla como un valor literal y responder con un rechazo controlado o un resultado vacío. Nunca debe exponer un error de base de datos.
Inyección de plantilla
Prueba expresiones como:
{{ 7*7 }}
{{ config.__class__ }}
Úsalas en campos de nombre, etiqueta, asunto o configuración. Si la respuesta contiene 49, un motor de plantillas evaluó la entrada. Eso indica una posible inyección de plantillas del lado del servidor.
Deserialización insegura y cargadores de código remoto
Envía un cargador no admitido:
{
"loader": "pickle://s3/models/payload.pkl"
}
También prueba objetos serializados en campos que deberían aceptar valores simples. El endpoint debe rechazar cargadores desconocidos mediante una lista blanca; no debe intentar inferir o ejecutar formatos «útiles».
Inyección de comandos
En campos que puedan terminar en argumentos de shell, como nombres de archivo u opciones de conversión, prueba:
; id
$(id)
Un 200 que expone un identificador de usuario o salida de shell es un hallazgo crítico.
Tamaño excesivo, malformación y confusión de tipo de contenido
No toda entrada hostil contiene una cadena ingeniosa. Algunas simplemente son demasiado grandes o tienen una forma inválida. A menudo dañan el parser antes de que se ejecute la validación de negocio.
Incluye estos casos en la suite:
- Un campo con cinco megabytes de un solo carácter.
- Un array JSON con un millón de elementos.
- JSON truncado.
- JSON con coma final.
- JSON anidado mil niveles de profundidad.
- Un cuerpo XML enviado con
Content-Type: application/json. - Un JSON enviado como
text/plain. - Un XML con entidad externa enviado como
application/xmlpara sondear XXE.
La respuesta esperada depende del caso:
| Caso | Respuesta esperada |
|---|---|
| Cuerpo demasiado grande | 413 Payload Too Large |
| JSON inválido | 400 Bad Request |
| Tipo de contenido no admitido |
415 Unsupported Media Type o 400
|
| Profundidad excesiva | Rechazo rápido, sin bloqueo ni 5xx
|
El servidor debe exigir coherencia entre el encabezado Content-Type y el cuerpo antes de intentar analizarlo.
Por qué los agentes de IA aumentan los riesgos
Estos riesgos existían antes de los agentes de IA. Lo que cambia es el volumen y la velocidad.
Una persona envía una solicitud hostil por vez. Un agente puede generar, reenviar, mutar y reintentar cargas a velocidad de máquina.
Tres propiedades aumentan la exposición:
- Sintetizan entradas. Generan combinaciones de valores que ningún humano escribió y que quizá ninguna prueba anticipó.
- Reintentan y encadenan llamadas. Un documento o webhook envenenado puede provocar miles de solicitudes en segundos.
- Reenvían datos de fuentes en las que confían. Una carga oculta en un dataset o webhook puede cruzar límites de confianza y llegar a tu API.
El patrón de «cargar estos datos» que se convierte en «ejecutar este código» es precisamente el tipo de instrucción que un agente puede transportar sin reconocerla como peligrosa. La inyección de prompts para equipos de API profundiza en ese traspaso.
La defensa no cambia: validar estrictamente, rechazar entradas inesperadas y probar los límites. Lo que cambia es que debe ejecutarse de forma automática.
Construye la suite negativa y ejecútala en CI con cada cambio
Convierte los casos anteriores en una suite que se ejecute en cada pull request. Este ejemplo con pytest prueba un endpoint de staging y verifica que las cargas hostiles se rechacen correctamente:
import httpx
import pytest
BASE = "https://staging.internal/v1"
HOSTILE_CONFIGS = [
{"loader": "pickle://s3/models/payload.pkl", "format": "auto"},
{"loader": "csv", "name": "{{ 7*7 }}"},
{"loader": "csv", "name": "{{ config.__class__ }}"},
{"loader": "csv", "filter": "1); DROP TABLE datasets;--"},
{"loader": "csv", "name": "A" * 5_000_000},
]
@pytest.mark.parametrize("config", HOSTILE_CONFIGS)
def test_dataset_config_is_refused(config):
response = httpx.post(
f"{BASE}/datasets",
json={"config": config},
timeout=10,
)
assert response.status_code in (400, 413, 422), response.text
assert response.status_code < 500, (
"Un 5xx indica que la carga alcanzó lógica que no debía procesarla"
)
assert "49" not in response.text, (
"La plantilla se evaluó: posible inyección de plantilla del lado del servidor"
)
Añade casos específicos para cuerpos inválidos y tipos de contenido:
def test_rejects_malformed_json():
response = httpx.post(
f"{BASE}/datasets",
content=b'{"config": {"loader": "csv",}}',
headers={"Content-Type": "application/json"},
timeout=10,
)
assert response.status_code == 400
assert response.status_code < 500
def test_rejects_wrong_content_type():
response = httpx.post(
f"{BASE}/datasets",
content=b"<config><loader>csv</loader></config>",
headers={"Content-Type": "application/json"},
timeout=10,
)
assert response.status_code in (400, 415)
assert response.status_code < 500
Después, bloquea las fusiones si la suite falla. Un flujo mínimo de GitHub Actions:
name: api-abuse-tests
on: [push, pull_request]
jobs:
negative-input:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pytest tests/negative_input.py -q
Ejecuta estas pruebas contra staging o un entorno aislado, nunca directamente contra producción. Algunas cargas están diseñadas para consumir recursos y otras podrían modificar datos si descubren un fallo.
Añade los escenarios al contrato de API
Una herramienta centrada en esquemas resulta útil cuando permite mantener los escenarios negativos junto a los de ruta feliz.
En Apidog, puedes diseñar el endpoint desde un contrato OpenAPI y verificar solicitudes y respuestas contra ese contrato durante las pruebas. Guarda escenarios para:
- Campos sobredimensionados.
- Tipos incorrectos.
- Propiedades no permitidas.
- Cargadores no incluidos en la lista blanca.
- Cadenas de inyección.
- Cuerpos mal formados.
- Tipos de contenido incorrectos.
Para cada escenario, añade una aserción de estado 4xx. Después, ejecuta los mismos escenarios en CI mediante la CLI de Apidog para que una relajación accidental de la validación falle la compilación antes del despliegue.
Si quieres probarlo, descarga Apidog y añade un escenario negativo a un endpoint existente.
El límite es claro: Apidog es una herramienta de diseño, prueba, simulación y documentación. No ejecuta un WAF, no filtra tráfico en vivo, no reemplaza un SIEM y la validación de contratos no detecta todos los exploits. Su valor está en hacer explícito el contrato y evitar que la categoría de «nunca verificamos qué acepta este endpoint» llegue a producción.
Preguntas frecuentes
¿Cuál es la diferencia entre las pruebas negativas y el fuzzing?
Las pruebas negativas envían un conjunto curado de entradas incorrectas, seleccionadas para cubrir fallos concretos. El fuzzing envía grandes volúmenes de entradas aleatorias o mutadas para descubrir casos no anticipados.
Empieza con pruebas negativas: son rápidas, deterministas y adecuadas para CI. Añade fuzzing cuando necesites más amplitud.
¿Deberían ejecutarse estas pruebas en producción?
No. Ejecútalas en staging o en un entorno aislado. Las cargas sobredimensionadas, las sondas de comandos y otros casos agresivos pueden estresar el sistema o modificar datos si existe un fallo.
¿Un WAF no detectaría esto de todos modos?
Un WAF es una capa útil de defensa en profundidad, pero no reemplaza la validación de la aplicación. Sus reglas pueden evadirse y no conocen tu lógica de negocio. Estas pruebas demuestran que el endpoint rechaza por sí mismo las entradas incorrectas.
¿Cuántos casos negativos son suficientes por endpoint?
Cubre cada campo con al menos un caso por clase de fallo relevante:
- Tipo incorrecto.
- Valor fuera de rango.
- Valor demasiado largo.
- Campo prohibido.
- Campo obligatorio ausente.
- Formato inválido.
- Cadenas de inyección adecuadas al contexto.
Normalmente son unos pocos casos por endpoint, no cientos. La cobertura de clases de fallo importa más que el recuento bruto.
¿La validación de esquema detiene por completo la inyección?
No. Un esquema estricto reduce entradas mal formadas, sobredimensionadas e inesperadas, pero un valor puede ser válido según el esquema y seguir siendo peligroso en una consulta SQL o una plantilla.
Mantén consultas parametrizadas, deserialización segura, codificación de salida y controles de autorización. Usa el esquema para reducir la superficie que esas capas deben defender.
Top comments (0)