Schemathesis

Informe de Fuzzing Automatizado

NutriTrack API v1  ·  staging.nutritrack.io  ·  OpenAPI 3.0
Iniciado: 2026-06-16 09:14:32 UTC
Completado: 2026-06-16 09:47:18 UTC
Duración: 32 min 46 s  ·  Workers: 4
Ejecución completada con 4 fallos críticos y 3 advertencias. La API NO supera las comprobaciones de seguridad mínimas.
st run .../openapi.json --checks all --stateful=links --hypothesis-max-examples=500
Fallos Críticos
4
HTTP 500 / Inyección
Advertencias
3
Violación contrato
Tests Ejecutados
14 872
6 endpoints × 500 ej
Tests Pasados
14 865
99.95% éxito
Cobertura Schema
100%
6 / 6 endpoints
Schema cargado
6 rutas · 23 params
Generación
14 872 casos
Ejecución
32 min · 4 workers
Shrinking
4 casos mínimos
Verificación
4 críticos · FALLÓ
Hallazgos 4 críticos 3 advertencias
Crítico
HTTP 500 — Crash en validación de peso negativo
POST/api/v1/patients  ·  Campo: peso_kg  ·  Reproducibilidad: 100%
500 Server Error Input Validation

Descripción: Al enviar peso_kg: -2147483648 (valor mínimo de int32), FastAPI propaga la excepción sin capturarla, devolviendo un traceback completo de Python en el cuerpo de la respuesta HTTP.

Input mínimo reproducible:

# curl equivalente generado por Schemathesis curl -X POST https://staging.nutritrack.io/api/v1/patients \ -H "Authorization: Bearer eyJhbG..." \ -H "Content-Type: application/json" \ -d '{ "nombre": "A", "email": "a@b.co", "fecha_nacimiento": "2000-01-01", "altura_cm": 1, "peso_kg": -2147483648 ← valor overflow int32 }' # Respuesta obtenida: HTTP 500 Internal Server Error { "detail": "Traceback (most recent call last):\n File \"/app/routers/patients.py\", line 47...\n ValueError: peso_kg must be positive\n File \"/app/db/models.py\", line 12..." }

Riesgo: El traceback expone rutas internas (/app/routers/, /app/db/), nombres de módulos y lógica de negocio. Debe añadirse validación ge=0 en el modelo Pydantic y captura global de excepciones.

Crítico
Filtración de SQL en respuesta de error — Inyección potencial
GET/api/v1/patients/{patient_id}  ·  Campo: patient_id  ·  Reproducibilidad: 100%
Info Disclosure SQL Leak

Descripción: Con patient_id = "1 OR 1=1", el endpoint devuelve un error 422 cuyo detalle incluye la query SQL fallida, confirmando que el parámetro se interpola sin sanitizar antes de la validación de tipo.

curl https://staging.nutritrack.io/api/v1/patients/1%20OR%201%3D1 \ -H "Authorization: Bearer eyJhbG..." HTTP 422 Unprocessable Entity { "detail": "invalid input syntax for type integer: '1 OR 1=1'\n QUERY: SELECT * FROM patients WHERE id = '1 OR 1=1'", "hint": "Check psycopg2 driver version" }

Riesgo: El mensaje de error revela la query SQL literal, el driver de base de datos (psycopg2) y que se usa interpolación de strings. Aunque FastAPI valida el tipo antes de ejecutar la query, el mensaje de error expone la estructura interna. Usar ge=1 en el path param y capturar excepciones de DB sin propagar mensajes raw.

Crítico
HTTP 500 — Crash al registrar comida con kcal = null
PUT/api/v1/patients/{patient_id}/meals  ·  Campo: kcal  ·  Reproducibilidad: 87%
500 Server Error Null Handling

Descripción: El schema OpenAPI declara kcal como type: number sin marcar required. Schemathesis genera payloads donde kcal es null, lo que provoca un NullPointerException en el cálculo de macros.

curl -X PUT https://staging.nutritrack.io/api/v1/patients/42/meals \ -H "Authorization: Bearer eyJhbG..." \ -d '{"nombre": "Paella", "kcal": null, "proteinas_g": 18.5}' HTTP 500 Internal Server Error {"detail": "unsupported operand type(s) for /: 'NoneType' and 'int'"}

Fix: Añadir kcal: float = Field(..., gt=0) en MealCreate (campo requerido y positivo). Actualizar el schema OpenAPI para marcarlo required: true.

Crítico
Respuesta del informe viola el contrato OpenAPI — campo faltante
GET/api/v1/reports/patient/{patient_id}  ·  Campo faltante: imc  ·  Frecuencia: 34% de los casos
Schema Violation Contract

Descripción: El schema declara que la respuesta del informe SIEMPRE incluye imc (Índice de Masa Corporal). Sin embargo, cuando el paciente no tiene registrada la altura, el campo se omite silenciosamente en lugar de devolver null o un error.

# Schema declara: ReportResponse: required: [patient_id, periodo, imc, kcal_media, balance_proteico] properties: imc: {type: number} # Respuesta real (patient sin altura_cm): HTTP 200 OK { "patient_id": 7, "periodo": "2026-05", // ← "imc" AUSENTE: violación de contrato "kcal_media": 1840.5, "balance_proteico": "déficit" }

Riesgo: Los clientes (apps móviles, dashboards) que lean response.imc sin guardia obtendrán undefined / excepción. El schema debe marcarlo nullable: true o garantizar su presencia.

Aviso
Respuesta lenta (4.8s) en DELETE con IDs aleatorios — posible DoS
DELETE/api/v1/patients/{patient_id}  ·  P99: 4.8s  ·  Umbral: 5s
Performance DoS Risk

Descripción: El 99th percentil de latencia del endpoint DELETE supera los 4.8 segundos con patient_id de gran magnitud (ej. 99999999), rozando el umbral de 5s. Sugiere ausencia de índice en la columna id o una consulta de cascada no optimizada al borrar dependencias.

Aviso
Auth token devuelve 200 con credenciales vacías (string vacío)
POST/api/v1/auth/token  ·  Input: username="", password=""
Auth Bypass Validation

Descripción: El endpoint de login devuelve 401 correctamente con contraseña incorrecta, pero con username="" y password="" devuelve 422 en lugar de 400. El schema declara que las credenciales vacías deben retornar 400 Bad Request.

Info
Header X-Request-ID ausente en respuestas de error
Todos los endpoints  ·  Header: X-Request-ID
Observability Schema

Descripción: El schema declara X-Request-ID como header de respuesta en todos los endpoints. En las respuestas 4xx/5xx el header no se incluye, lo que dificulta la correlación de errores en los logs de producción.


Cobertura por Endpoint 6 / 6
POST /api/v1/patients
2 484
Pasados
16
Fallidos
99% pass
GET /api/v1/patients/{patient_id}
2 497
Pasados
3
Fallidos
99.8% pass
PUT /api/v1/patients/{patient_id}/meals
2 436
Pasados
64
Fallidos
97.4% pass
POST /api/v1/auth/token
2 500
Pasados
0
Fallidos
100% pass
GET /api/v1/reports/patient/{patient_id}
2 330
Pasados
170
Fallidos
93.2% pass
DELETE /api/v1/patients/{patient_id}
2 498
Pasados
2
Fallidos
99.9% pass

Integración en CI/CD — GitHub Actions
# .github/workflows/api-fuzzing.yml name: API Fuzzing — NutriTrack on: [push, pull_request] jobs: schemathesis: runs-on: ubuntu-latest services: api: image: ghcr.io/nutritrack/api:${{ github.sha }} ports: ["8000:8000"] env: DATABASE_URL: postgresql://postgres:test@postgres/nutritrack steps: - uses: actions/checkout@v4 - name: Run Schemathesis fuzzing uses: schemathesis/action@v1 with: schema: http://localhost:8000/api/v1/openapi.json args: | --checks all --stateful=links --hypothesis-max-examples=200 --hypothesis-deadline=5000 --workers 4 --junit-xml=results.xml --header "Authorization: Bearer ${{ secrets.STAGING_API_TOKEN }}" - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: schemathesis-results path: results.xml

Plan de Acción Prioritario
🔴
Añadir validación Pydantic estricta en modelos de entrada P0 — Hoy
Añadir ge=0 en peso_kg, altura_cm y gt=0 en kcal. FastAPI rechazará los valores antes de llegar a la lógica de negocio, eliminando los 500s.
🔴
Handler global de excepciones con mensajes sanitizados P0 — Hoy
Registrar un @app.exception_handler(Exception) que loguee el traceback internamente pero devuelva solo {"detail": "Internal server error", "request_id": "..."} al cliente.
🟠
Corregir schema OpenAPI: marcar imc como nullable o required P1 — Esta semana
Decidir si imc puede ser null (paciente sin altura) y actualizar el schema. Los clientes deben poder confiar en el contrato sin guardias defensivas.
🟠
Índice en patients.id y optimizar cascada de DELETE P1 — Esta semana
Añadir CREATE INDEX IF NOT EXISTS idx_patients_id ON patients(id); y revisar la estrategia de borrado en cascada para reducir latencia de P99 por debajo de 1s.
🟢
Integrar Schemathesis en pipeline CI/CD con fallo automático P2 — Este mes
Usar la configuración YAML generada arriba. Con 200 ejemplos por endpoint el CI tarda ~8 minutos. Cualquier 500 o violación de schema bloquea el merge.