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, comoexecute()osubprocess.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 usuarioanapuede ver la nota privada debeto. -
V2: al hacer ping a
127.0.0.1 && whoami, el servidor ejecutawhoami.
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
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
Paso 3. Ejecutar las consultas y exportar el resultado SARIF
codeql database analyze codeql-db \
--format=sarif-latest \
--output=codeql.sarif
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
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)
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/
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 }}"
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}%")
)
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
)
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"]
V4. Contraseñas: sustituir MD5 por scrypt de Werkzeug
generate_password_hash(password)
check_password_hash(hash, password)
V5. Modo debug: desactivado por defecto
app.run(debug=os.environ.get("FLASK_DEBUG", "0") == "1")
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ó porpy/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:
- En render.com, crear New → Web Service y seleccionar el repositorio
Laos19/sast-flask-demo. - Configurar el comando de construcción como
pip install -r requirements.txty el comando de inicio comogunicorn app.app:app. Seleccionar una instancia gratuita. - Configurar las variables de entorno
SECRET_KEY—generada desde Render— yFLASK_DEBUG=0. - 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
- OWASP Foundation. Source Code Analysis Tools. https://owasp.org/www-community/Source_Code_Analysis_Tools
- NIST. Source Code Security Analyzers. https://www.nist.gov/itl/csd/secure-systems-and-applications/source-code-security-analyzers
- PyCQA. Bandit documentation. https://bandit.readthedocs.io/
- GitHub. CodeQL documentation. https://codeql.github.com/docs/
- GitHub. About code scanning (SARIF). https://docs.github.com/en/code-security/code-scanning
- Render. Deploy Hooks. https://render.com/docs/deploy-hooks
Etiquetas sugeridas para DEV Community: security, python, github, webdev
Top comments (0)