Buscar en la documentación

Escaneo con OWASP ZAP

Cómo correr el escaneo pasivo de la SPA y el activo de la API con ZAP en el Docker local, y qué encontró.

2 min de lecturaVer en GitHub

ZAP corre como perfil opcional del Docker local (zap): docker compose up -d no lo levanta. Solo se usa contra el stack local; los reportes quedan en zap-reports/, fuera de Git (ADR-0060).

Escaneo pasivo de la SPA

Recorre la app y revisa cabeceras, cookies y respuestas sin atacar nada:

docker compose -p trazo --profile zap run --rm zap zap-baseline.py -t http://gateway:8080 -r baseline.html -J baseline.json -I -j

En Git Bash para Windows, antepón MSYS_NO_PATHCONV=1 a los comandos de ZAP: si no, convierte las rutas /zap/... en rutas de Windows.

Escaneo activo de la API

Ataca cada ruta del contrato OpenAPI con la sesión de una cuenta de prueba. Cambia datos de demostración: conviene respaldar la base antes y restaurarla al terminar.

  1. Subir el límite general de la API, para que ZAP no reciba solo respuestas 429:

    API_RATE_LIMIT=100000 docker compose -p trazo up -d --wait app
  2. Emitir un token de prueba. Solo existe en desarrollo y no pasa por el MFA:

    docker compose -p trazo exec app php artisan trazo:dev-token superadmin@trazo.test
  3. Correr el plan, con el token en TRAZO_TOKEN:

    API_RATE_LIMIT=100000 docker compose -p trazo --profile zap run --rm --no-deps -e TRAZO_TOKEN=<token> zap zap.sh -cmd -autorun /zap/config/api-scan.yaml
  4. Volver el límite a 120 (docker compose -p trazo up -d app), revocar el token y restaurar los datos.

Qué hace el plan (docker/zap/api-scan.yaml):

  • Importa el contrato y agrega el token a cada petición.
  • Excluye cerrar sesión, que revocaría el token del escaneo, y el contrato, que tarda varios segundos en generarse.
  • Ataca durante un máximo de 45 minutos y escribe api.html y api.json desde dentro de ZAP.

Se usa el plan de automatización porque el script zap-api-scan.py se cortaba al bajar las alertas.

Resultados (25 de octubre de 2026)

EscaneoAlertaRiesgoQué se hizo
PasivoNon-Storable ContentInformativaCorrecto: las respuestas de la API no se guardan en caché
Pasivo y activoZAP is Out of DateBajaAviso de la herramienta: la versión está fijada (2.16.1) a propósito
ActivoCross-Domain Misconfiguration (Access-Control-Allow-Origin: *)MediaCorregido: CORS cerrado (cabeceras)
ActivoCSP: style-src unsafe-inlineMediaSolo en local, por Vite. En producción la CSP es estricta y está comprobada
ActivoAuthentication Request IdentifiedInformativaZAP reconoce el login y la recuperación; nada que corregir
ActivoSensitive Information in URL (filter[user])InformativaFalso positivo: es el ULID de la persona del filtro, no un dato secreto

Ninguna alerta alta: sin inyección SQL, XSS, inclusión de rutas, ejecución remota ni fallas de control de acceso detectadas en 163 rutas con sus variantes. El escaneo activo llegó al tope de 45 minutos; una corrida más larga podría probar más variantes por parámetro.

Dónde está en el código