DEV Community

CodeQL en la práctica: rastreando vulnerabilidades en una app Flask con GitHub Actions y Render

CodeQL en la práctica: rastreando vulnerabilidades en una app Flask con GitHub Actions y Render

Autora: Adriana Laos González

Curso: Calidad y Pruebas de Software (SI784)

Institución: Universidad Privada de Tacna

Repositorio: https://github.com/Laos19/sast-flask-demo

Aplicación desplegada: https://sast-flask-demo.onrender.com

Artículo complementario (Bandit): [Agregar enlace]

Video (máximo 5 minutos): [Agregar enlace]

1. ¿Por qué otra herramienta SAST?

En los laboratorios del curso ya usamos SonarCloud, Snyk y Semgrep. Para este trabajo buscamos en la lista Source Code Analysis Tools de OWASP y en la de NIST herramientas que no hubiéramos usado. Mi compañero eligió Bandit y yo elegí CodeQL, que en la lista de OWASP aparece dentro de la entrada GitHub Advanced Security.

La primera herramienta que tenía asignada era Bearer CLI. La descarté porque sobre nuestra aplicación solo detectó 1 de 5 vulnerabilidades. Revisando sus reglas oficiales (Bearer/bearer-rules), vi que, en Python, sus fuentes de “entrada externa” incluyen Django, input(), sys.argv y otras, pero no flask.request. Por eso no detecta las inyecciones en una aplicación Flask. La lección es que, antes de adoptar una herramienta SAST, hay que comprobar que entiende el framework que utiliza el proyecto.

SAST (Static Application Security Testing) significa analizar el código fuente sin ejecutarlo para encontrar patrones o flujos inseguros antes de que lleguen a producción.

2. ¿Qué es CodeQL?

  • Mantenedor: GitHub (Microsoft); procede de Semmle, adquirida en 2019.
  • Técnica: convierte el código en una base de datos que se consulta mediante el lenguaje QL.
  • Característica diferencial: utiliza taint tracking para seguir un dato desde la fuente —por ejemplo, request.args— hasta un destino peligroso, como execute() o subprocess.run().
  • Lenguajes: C/C++, C#, Go, Java/Kotlin, JavaScript/TypeScript, Python, Ruby, Swift, entre otros.
  • Licencia y disponibilidad: la CLI es gratuita para código abierto e investigación académica. Las consultas están disponibles en el repositorio github/codeql. Los repositorios privados comerciales requieren GitHub Advanced Security.

Una herramienta basada en patrones puede ver una línea como execute(f"... {q} ...") y avisar porque “parece SQL armado con f-string”. CodeQL, en cambio, puede demostrar que q proviene del usuario y llega sin sanitizar a execute(). Así se reducen los falsos positivos y cada hallazgo incluye una explicación.

3. La aplicación de prueba: NotasApp

Creamos una aplicación pequeña con Python 3.12, Flask, SQLite y pytest. Tiene registro e inicio de sesión, notas personales, un buscador y una utilidad para hacer ping a un host. Introdujimos cinco vulnerabilidades de forma intencional:

ID Vulnerabilidad Ubicación CWE
V1 SQL Injection mediante f-string buscar() CWE-89
V2 Command Injection mediante shell=True ping() CWE-78
V3 Secreto escrito directamente en el código (SECRET_KEY = "super-secreto-123") app.py CWE-798
V4 Uso de MD5 para contraseñas hash_password() CWE-327
V5 app.run(debug=True) Arranque de la aplicación CWE-489

Las comprobamos localmente antes de escanear:

  • V1: al buscar ' OR 1=1 --, el usuario ana puede ver la nota privada de beto.
  • V2: al hacer ping a 127.0.0.1 && whoami, el servidor ejecuta whoami.

El repositorio contiene dos etiquetas (tags): v1-vulnerable y v2-corregido.

4. Usar CodeQL en local

Descargamos el paquete oficial de CodeQL CLI y sus consultas. El paquete comprimido ocupa alrededor de 700 MB.

Paso 1. Descargar y descomprimir el paquete

curl -L -o codeql-bundle.tar.gz \
  https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.27.1/codeql-bundle-win64.tar.gz

tar -xzf codeql-bundle.tar.gz
Enter fullscreen mode Exit fullscreen mode

Paso 2. Crear la base de datos del código

codeql database create codeql-db \
  --language=python \
  --source-root=. \
  --codescanning-config=.github/codeql/codeql-config.yml \
  --overwrite
Enter fullscreen mode Exit fullscreen mode

Paso 3. Ejecutar las consultas y exportar el resultado SARIF

codeql database analyze codeql-db \
  --format=sarif-latest \
  --output=codeql.sarif
Enter fullscreen mode Exit fullscreen mode

Con la suite predeterminada, CodeQL detectó cuatro de las cinco vulnerabilidades: no encontró el secreto escrito en el código (V3). Para detectarlo, activamos una consulta experimental en .github/codeql/codeql-config.yml:

name: "CodeQL NotasApp"
queries:
  - uses: security-extended
packs:
  python:
    - codeql/python-queries:experimental/Security/CWE-287-ConstantSecretKey/WebAppConstantSecretKey.ql
paths:
  - app
Enter fullscreen mode Exit fullscreen mode

Resultados sobre la versión vulnerable: 5 de 5

Regla Severidad Línea Vulnerabilidad
py/command-line-injection 9.8 — Critical 128 V2
py/sql-injection 8.8 — High 115 V1
py/flask-constant-secret-key (experimental) 8.5 — High 19 V3
py/weak-sensitive-data-hashing 7.5 — High 29 V4
py/flask-debug 7.5 — High 136 V5

Todos los hallazgos aparecieron con precisión alta y ninguno fue un falso positivo. Para V1, CodeQL muestra el recorrido completo del dato:

request.args.get("q") (línea 110)
    → q
    → consulta (línea 114)
    → execute(consulta) (línea 115)
Enter fullscreen mode Exit fullscreen mode

También distingue que MD5 se aplica a una contraseña y no a cualquier dato. Por eso la regla se llama weak-sensitive-data-hashing.

5. Automatización: GitHub Actions + quality gate

El pipeline .github/workflows/ci-sast-deploy.yml contiene cuatro trabajos (jobs): tests (pytest), sast-bandit, sast-codeql y deploy. El trabajo de CodeQL es el siguiente:

sast-codeql:
  runs-on: ubuntu-latest
  permissions:
    contents: read
    actions: read
    security-events: write # Para publicar en Code Scanning
  steps:
    - uses: actions/checkout@v4
    - uses: github/codeql-action/init@v4
      with:
        languages: python
        config-file: ./.github/codeql/codeql-config.yml
    - uses: github/codeql-action/analyze@v4
      with:
        category: codeql-python
        output: codeql-sarif
    - name: Quality gate CodeQL (severidad alta)
      run: python3 scripts/sarif_gate.py codeql-sarif/python.sarif 7.0
    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: reporte-codeql
        path: codeql-sarif/
Enter fullscreen mode Exit fullscreen mode

En GitHub Actions no hace falta instalar CodeQL manualmente, porque codeql-action/init lo descarga automáticamente. Los resultados aparecen en Security → Code scanning.

Un detalle importante: la acción oficial de CodeQL no hace fallar el pipeline solo por encontrar vulnerabilidades; únicamente las reporta. Para establecer un quality gate real, escribimos un script (scripts/sarif_gate.py) que lee el archivo SARIF y termina con código de salida 1 si encuentra algún hallazgo con security-severity >= 7.0.

El despliegue solo ocurre si los tres trabajos anteriores pasan y el evento es un push a main:

deploy:
  needs: [tests, sast-bandit, sast-codeql]
  if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
  runs-on: ubuntu-latest
  steps:
    - run: curl -fsS -X POST "${{ secrets.RENDER_DEPLOY_HOOK }}"
Enter fullscreen mode Exit fullscreen mode

Con los gates activos, los tiempos de ejecución observados fueron: pruebas, 9 segundos; Bandit, 18 segundos; CodeQL, 39 segundos; y despliegue, 3 segundos.

6. Corrección: de 5 hallazgos a 0

Hicimos un commit por cada vulnerabilidad.

V1. SQL Injection: consulta parametrizada

db.execute(
    "SELECT id, titulo, contenido FROM notas WHERE usuario_id = ? AND titulo LIKE ?",
    (session["usuario_id"], f"%{q}%")
)
Enter fullscreen mode Exit fullscreen mode

V2. Command Injection: evitar el shell y validar el host

if host_valido(host):
    subprocess.run(
        [ejecutable, opcion, "1", host],
        capture_output=True,
        text=True,
        timeout=10
    )
Enter fullscreen mode Exit fullscreen mode

Se utiliza una lista de argumentos en lugar de shell=True, y el host se valida con ipaddress y una expresión regular.

V3. Secreto en el código: variable de entorno

app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
Enter fullscreen mode Exit fullscreen mode

V4. Contraseñas: sustituir MD5 por scrypt de Werkzeug

generate_password_hash(password)
check_password_hash(hash, password)
Enter fullscreen mode Exit fullscreen mode

V5. Modo debug: desactivado por defecto

app.run(debug=os.environ.get("FLASK_DEBUG", "0") == "1")
Enter fullscreen mode Exit fullscreen mode

Después de las correcciones, los resultados fueron:

Severidad Antes Después
Critical 1 0
High 4 0
Total 5 0

Tras corregir, no quedaron alertas de CodeQL. La herramienta entiende que, en subprocess.run([...]), el host es un argumento y no un comando. Bandit, en cambio, sigue mostrando allí una advertencia informativa (B603). Además, aumentamos las pruebas de pytest de cuatro a siete, incorporando pruebas de regresión para V1, V2 y V4.

7. Probando el quality gate

En la rama demo-gate volvimos a introducir V2 (shell=True) y abrimos el PR #1. El resultado fue:

  • tests: falló la prueba de regresión de V2.
  • sast-bandit: falló por la regla B602, severidad alta.
  • sast-codeql: falló por py/command-line-injection, con severidad 9.8.
  • deploy: omitido; no se desplegó la aplicación.

En el pull request, CodeQL comenta la línea afectada y propone un Suggested changeset (autocorrección). Esta prueba permitió comprobar que la vulnerabilidad reintroducida queda bloqueada antes de llegar a producción.

8. Despliegue en Render (nube pública, plan gratuito)

Para desplegar la aplicación:

  1. En render.com, crear New → Web Service y seleccionar el repositorio Laos19/sast-flask-demo.
  2. Configurar el comando de construcción como pip install -r requirements.txt y el comando de inicio como gunicorn app.app:app. Seleccionar una instancia gratuita.
  3. Configurar las variables de entorno SECRET_KEY —generada desde Render— y FLASK_DEBUG=0.
  4. Desactivar Auto-Deploy, copiar el Deploy Hook y guardarlo en GitHub como el secreto RENDER_DEPLOY_HOOK.

Por seguridad, solo desplegamos la versión corregida. Publicar v1-vulnerable habría dejado una ejecución remota de comandos expuesta en internet. Un detalle observado fue que Render tiene a Cloudflare delante y su WAF devolvió 403 Blocked al payload ' OR 1=1 --. Esto constituye defensa en profundidad, pero no reemplaza la corrección del código.

9. Problemas que encontramos

Problema Solución
Bearer CLI no reconoce flask.request y detecta solo 1 de 5 vulnerabilidades. Cambiar a CodeQL después de revisar sus reglas.
CodeQL no detecta la clave SECRET_KEY fija con la suite estándar. Activar la consulta experimental py/flask-constant-secret-key.
La acción de CodeQL no bloquea el pipeline ante hallazgos. Añadir sarif_gate.py con umbral 7.0.
Render muestra Failed to find attribute 'app' in 'app'. Usar gunicorn app.app:app como comando de inicio.
El paquete de CodeQL es pesado (aproximadamente 1,5 GB descomprimido). En CI no hace falta instalarlo manualmente: la acción oficial lo descarga.

10. Conclusiones

Ventajas

  • Realiza análisis del flujo de datos.
  • No produjo falsos positivos en los hallazgos observados y no dejó alertas después de las correcciones.
  • Se integra de forma nativa con GitHub mediante Code Scanning, comentarios en pull requests y sugerencias de autocorrección.
  • Soporta numerosos lenguajes de programación.

Limitaciones

  • Es pesado: en las pruebas locales tardó unas 50 veces más que Bandit.
  • La detección de la clave fija en el código requirió una consulta experimental.
  • Para implementar un quality gate fue necesario crear un script propio.
  • La licencia gratuita está limitada a código abierto o investigación académica; los repositorios privados comerciales requieren GitHub Advanced Security.

Recomendamos combinar ambas herramientas: Bandit como filtro rápido en cada commit y CodeQL para obtener hallazgos más precisos y explicados. En nuestro pull request de prueba, las dos herramientas bloquearon la vulnerabilidad reintroducida antes de que llegara a producción.

Referencias

  1. OWASP Foundation. Source Code Analysis Tools. https://owasp.org/www-community/Source_Code_Analysis_Tools
  2. NIST. Source Code Security Analyzers. https://www.nist.gov/itl/csd/secure-systems-and-applications/source-code-security-analyzers
  3. PyCQA. Bandit documentation. https://bandit.readthedocs.io/
  4. GitHub. CodeQL documentation. https://codeql.github.com/docs/
  5. GitHub. About code scanning (SARIF). https://docs.github.com/en/code-security/code-scanning
  6. Render. Deploy Hooks. https://render.com/docs/deploy-hooks

Etiquetas sugeridas para DEV Community: security, python, github, webdev

Top comments (0)