DAST y escaneo de contenedores en CI/CD: auditando una app Flask con Wapiti y Trivy
Autor: Stevie Gerald Marca Aguilar , Usher Damiron Halanocca Rojas Pipeline DevSecOps que construye una aplicación web, la ataca con un escáner dinámico, revisa su imagen Docker en busca de CVE y, al final, la publica au
Autor: Stevie Gerald Marca Aguilar , Usher Damiron Halanocca Rojas
Pipeline DevSecOps que construye una aplicación web, la ataca con un escáner dinámico, revisa su imagen Docker en busca de CVE y, al final, la publica automáticamente en la nube.
⚠️ La aplicación contiene vulnerabilidades a propósito (SQLi y XSS) con fines educativos. No la reutilices en un entorno real.
Tabla de contenidos
Contexto: seguridad desde el inicio
Herramientas elegidas
Flujo del pipeline
La aplicación de prueba
Workflow de GitHub Actions
Publicación en Render
Qué encontraron los escáneres
Cómo corregirlo
Ejecución local
ConclusionesEnlaces
- Contexto: seguridad desde el inicio Dejar la seguridad para la víspera del despliegue ya no es viable. La práctica DevSecOps, también llamada Shift-Left Security, propone que los controles de seguridad se ejecuten de forma automática desde las primeras etapas del desarrollo, igual que las pruebas unitarias. OWASP mantiene un catálogo público de herramientas de escaneo de vulnerabilidades. Para este proyecto se combinaron dos enfoques complementarios: uno que prueba la aplicación mientras está en ejecución y otro que inspecciona el contenedor donde vive.
- Herramientas elegidas Herramienta Enfoque Qué detecta Wapiti DAST (caja negra) Envía payloads maliciosos a la app en ejecución para descubrir XSS, inyección SQL, inclusión de archivos, ejecución de comandos y cabeceras HTTP débiles Trivy (Aqua Security) Escaneo de contenedores y dependencias CVE conocidas en la imagen Docker, en los paquetes del sistema operativo base y en las librerías de Python Wapiti ve la aplicación como la vería un atacante desde fuera; Trivy mira hacia dentro, a los componentes con los que fue construida. Juntos cubren tanto los errores en el código propio como los heredados de terceros.
- Flujo del pipeline Cada
pushamaindispara tres etapas en secuencia. Si una falla, las siguientes no se ejecutan:
push a main
│
▼
① Trivy → construye la imagen y busca CVE en SO y dependencias
│
▼
② Wapiti → levanta el contenedor y lo audita dinámicamente (DAST)
│
▼
③ Deploy → llama al Deploy Hook de Render y publica la app
- La aplicación de prueba
Para comprobar que los escáneres realmente detectan fallos, se creó una app mínima en Python 3.9 + Flask con una base SQLite en memoria. Incluye dos vulnerabilidades clásicas del OWASP Top 10 (categoría A03:2021 – Injection).
app/app.py
from flask import Flask, request
import sqlite3
app = Flask(__name__)
def db():
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER, name TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'admin'), (2, 'maria')")
return conn
@app.route("/")
def home():
return "<h1>Demo App</h1><a href='/search?q=maria'>Buscar</a> | <a href='/user?id=1'>Usuario</a>"
# Fallo intencional: XSS reflejado (CWE-79)
@app.route("/search")
def search():
q = request.args.get("q", "")
return f"<p>Resultados para: {q}</p>"
# Fallo intencional: inyección SQL (CWE-89)
@app.route("/user")
def user():
uid = request.args.get("id", "1")
rows = db().execute(f"SELECT * FROM users WHERE id = {uid}").fetchall()
return str(rows)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY app/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ .
EXPOSE 8000
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]
- Workflow de GitHub Actions
Archivo:
.github/workflows/pipeline.yml
name: Build, Scan and Deploy
on:
push:
branches: [main]
jobs:
scan-dependencies-and-image:
name: Trivy — Container and Dependency Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t demo-app:latest .
- name: Trivy — escaneo de imagen
uses: aquasecurity/trivy-action@master
with:
image-ref: demo-app:latest
format: table
output: trivy-report.txt
exit-code: 0
- uses: actions/upload-artifact@v4
with:
name: trivy-report
path: trivy-report.txt
dast-scan:
name: Wapiti — Dynamic Application Security Testing (DAST)
runs-on: ubuntu-latest
needs: scan-dependencies-and-image
steps:
- uses: actions/checkout@v4
- name: Levantar app
run: |
docker build -t demo-app .
docker run -d -p 8000:8000 demo-app
sleep 8
- name: Wapiti — escaneo dinámico
run: |
pip install wapiti3
wapiti -u http://localhost:8000/ -f html -o wapiti-report --scope folder
- uses: actions/upload-artifact@v4
with:
name: wapiti-report
path: wapiti-report
deploy:
name: Deploy to Cloud (Render)
runs-on: ubuntu-latest
needs: dast-scan
steps:
- name: Disparar despliegue en Render
run: curl -X POST "${{ secrets.RENDER_DEPLOY_HOOK }}"
Los reportes de Trivy y Wapiti quedan disponibles como artefactos descargables en cada ejecución.
- Publicación en Render
En Render se creó un Web Service con entorno Docker, vinculado al repositorio público.
En la configuración avanzada del servicio se generó una Deploy Hook URL.
Esa URL se guardó en GitHub como secret
RENDER_DEPLOY_HOOK. Así, el pipeline puede ordenar el despliegue al terminar los escaneos sin exponer ninguna credencial en el código. App en producción: https://vuln-scan-demo.onrender.com - Qué encontraron los escáneres
Wapiti (DAST)
Hallazgo Severidad Dónde Detalle
Inyección SQL Alta
/user?id=Al enviar valores como' OR '1'='1, la consulta cambia su lógica y devuelve todos los usuarios XSS reflejado Media/search?q=El parámetro se devuelve en el HTML sin codificar, por lo que un script inyectado se ejecuta en el navegador Cabeceras de seguridad ausentes Baja / Info Todas las rutas FaltanContent-Security-Policy,X-Frame-Options(riesgo de clickjacking) yX-Content-Type-Options: nosniffTrivy (contenedor y dependencias) Vulnerabilidades de severidad media y baja en paquetes del sistema base Debian depython:3.9-slim, comolibssl,glibcyzlib. CVE conocidas en la versión fijada deflasky en su dependenciaWerkzeug. - Cómo corregirlo Inyección SQL → consultas parametrizadas
@app.route("/user")
def user():
uid = request.args.get("id", "1")
rows = db().execute("SELECT * FROM users WHERE id = ?", (uid,)).fetchall()
return str(rows)
XSS → escapar la salida (o usar plantillas Jinja2, que escapan automáticamente)
from markupsafe import escape
@app.route("/search")
def search():
q = request.args.get("q", "")
return f"<p>Resultados para: {escape(q)}</p>"
Cabeceras HTTP → Flask-Talisman
from flask_talisman import Talisman
Talisman(app, content_security_policy={"default-src": "'self'"})
Hallazgos de Trivy
Migrar la imagen base a una versión con soporte, como python:3.12-slim o una variante Alpine.
Actualizar Flask (flask>=3.0.0) y Werkzeug, y añadir pip-audit al pipeline para vigilar las dependencias.
- Ejecución local
# Clonar
git clone https://github.com/UsherHalanoccaRojas/vuln-scan-demo.git
cd vuln-scan-demo
# Construir y ejecutar
docker build -t demo-app .
docker run -d -p 8000:8000 demo-app
# Trivy
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:latest image demo-app
# Wapiti
pip install wapiti3
wapiti -u http://localhost:8000/ -f html -o wapiti-report --scope folder
Configuración en GitHub
Secret Valor
RENDER_DEPLOY_HOOK Deploy Hook URL generada en Render
- Conclusiones Con herramientas open source como Wapiti y Trivy se puede montar una defensa sólida sin pagar licencias costosas. El escaneo continuo expone fallos graves, como SQLi y XSS, antes de que lleguen a producción. Los reportes guardados como artefactos dejan un registro de cada CVE detectada en la imagen y en las dependencias. El despliegue solo ocurre después de que los controles de seguridad se ejecutan. Enlaces
Repositorio: https://github.com/UsherHalanoccaRojas/vuln-scan-demo
App desplegada (Render): https://vuln-scan-demo.onrender.com
Video demostrativo: https://www.youtube.com/watch?v=04p1_29t7GQ
Catálogo OWASP de herramientas: https://community.owasp.org/Vulnerability_Scanning_Tools
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.