🔍
CULTIVA IA · Seguridad
CodeQL Static Analysis
✓ Análisis completado

Informe de Vulnerabilidades — NutriAPI Backend

Análisis de flujo de datos interprocedural con taint-tracking · Python 3.11 · FastAPI + SQLAlchemy
Repositorio nutriapi/backend
Lenguaje python
Build mode none
Fecha análisis 2026-06-16
Output dir static_analysis_codeql_1/
Duración 4m 38s
2
Crítico
4
Alto
6
Medio
9
Bajo
21
Total findings
Pipeline de análisis
🏗️
Build DB
340 archivos
📊
Quality Check
12.500 LoC · 0 errores
🔧
Data Extensions
7 modelos custom
🎯
Suite QLS
214 queries activas
📋
SARIF Output
results.sarif
🔍
Zero-finding check
21 findings reales
📁
340
Archivos Python extraídos
📏
12.547
Líneas de código (LoC)
⚙️
214
Queries ejecutadas
🔌
7
Data extensions custom (FastAPI/SQLAlchemy)
codeql/python-queries
112 queries · security-extended + experimental
trailofbits/python-queries
58 queries · Flask/FastAPI/async specific
githubsecuritylab/codeql-python-queries
44 queries · Community pack
Distribución de findings por categoría CWE
CWE-089 SQL Injection
SQL Injection
5
CWE-022 Path Traversal
Path Traversal
3
CWE-079 XSS
XSS
2
CWE-078 OS Command Injection
OS Command
2
CWE-502 Deserialization
Deser.
1
CWE-327 Weak Crypto
Weak Crypto
3
CWE-312 Cleartext Storage
Cleartext
2
Otras categorías
Otras
3
Findings críticos y altos (6 de 21)
# Severidad Vulnerabilidad CWE Archivo / Línea Descripción
01 ● CRÍTICO
SQL Injection via raw query
py/sql-injection
CWE-089
api/routes/search.py
Línea 87–91
Parámetro query de la petición HTTP se concatena directamente en db.execute() sin sanitizar. Permite exfiltrar tablas completas.
02 ● CRÍTICO
OS Command Injection
py/command-line-injection
CWE-078
workers/export.py
Línea 134
El nombre de archivo enviado por el usuario fluye hasta subprocess.run(shell=True). Un atacante puede ejecutar comandos arbitrarios en el host.
03 ● ALTO
Path Traversal en carga de archivos
py/path-injection
CWE-022
api/routes/uploads.py
Líneas 52, 58
filename del multipart/form-data fluye a open(os.path.join(UPLOAD_DIR, filename)) sin normalizar. Permite escribir archivos fuera del directorio designado.
04 ● ALTO
SQL Injection en filtro de ingredientes
py/sql-injection
CWE-089
api/routes/ingredients.py
Línea 201
Wrapper custom IngredientRepo.filter_by_tag() detectado por data-extension. Parámetro tag no modelado en SQLAlchemy ORM standard.
05 ● ALTO
Deserialización insegura (pickle)
py/unsafe-deserialization
CWE-502
cache/redis_cache.py
Línea 76
Datos de Redis deserializados con pickle.loads() sin validar origen. Si Redis está comprometido, permite RCE completo.
06 ● ALTO
JWT con algoritmo débil (HS256 hardcoded)
py/weak-cryptographic-algorithm
CWE-327
auth/tokens.py
Línea 23
Secret JWT de 16 bytes con algoritmo HS256 hardcoded. Longitud insuficiente para resistencia a fuerza bruta en HMAC.
Flujo de taint — Finding #01: SQL Injection (crítico)
S
Source — Entrada HTTP no confiable
request.query_params.get("query") — parámetro GET sin validar
api/routes/search.py:81
Propagación — asignación a variable local
search_term = request.query_params.get("query")
api/routes/search.py:81
Propagación — f-string interpolación
raw_sql = f"SELECT * FROM products WHERE name LIKE '%{search_term}%'"
api/routes/search.py:87
Propagación — paso como argumento
await db.execute(raw_sql) — inyectado como string SQL
api/routes/search.py:89
Sink — ejecución en base de datos PostgreSQL
asyncpg.Connection.execute() — ejecución directa de SQL arbitrario
api/routes/search.py:89 → asyncpg internals
api/routes/search.py — Líneas 78–93 (fragmento vulnerable) CRÍTICO · CWE-089
78 # GET /api/v1/search?query=...
79 @router.get("/search")
80 async def search_products(request: Request, db: AsyncSession = Depends(get_db)):
81 search_term = request.query_params.get("query") # ← SOURCE
82 if not search_term:
83 raise HTTPException(status_code=400)
84
85 # TODO: migrate to ORM — deuda técnica desde sprint 3
86 # WARNING: raw SQL temporal hasta que se mapee el modelo completo
87 raw_sql = f"SELECT * FROM products WHERE name LIKE '%{search_term}%'" # ← TAINT
88 results = await db.execute(raw_sql)
89 return [dict(row) for row in results.fetchall()] # ← SINK
90
91 # FIX: usar parámetros vinculados:
92 # stmt = select(Product).where(Product.name.ilike(f"%{search_term}%"))
93 # results = await db.execute(stmt)
static_analysis_codeql_1/results/results.sarif (extracto) SARIF 2.1.0
{
  "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json",
  "version": "2.1.0",
  "runs": [{
    "tool": { "driver": { "name": "CodeQL", "version": "2.21.1" }},
    "results": [{
      "ruleId": "py/sql-injection",
      "level": "error",
      "message": { "text": "This SQL query depends on user-provided value (query param 'query')" },
      "locations": [{ "physicalLocation": {
        "artifactLocation": { "uri": "api/routes/search.py" },
        "region": { "startLine": 87, "endLine": 89 }
      }}],
      "relatedLocations": [
        { "id": 1, "message": { "text": "Source: query param" },
          "physicalLocation": { "artifactLocation": { "uri": "api/routes/search.py" },
          "region": { "startLine": 81 }}},
        { "id": 2, "message": { "text": "Sink: asyncpg execute" },
          "physicalLocation": { "artifactLocation": { "uri": "api/routes/search.py" },
          "region": { "startLine": 89 }}}
      ]
    }, /* ... 20 findings más */]
  }]
}
Recomendaciones de remediación (prioridad)
🔐
Migrar todas las raw queries a SQLAlchemy ORM paramétrico
Reemplazar f-strings en db.execute() por select(Model).where(...) o text(sql).bindparams(...). Afecta search.py:87, ingredients.py:201, reports.py:145,162,178.
Crítico · 2 días
🛡️
Sanitizar filename en endpoint de uploads
Usar pathlib.Path(filename).name para extraer solo el nombre base, luego os.path.abspath() + prefijo check para prevenir path traversal. Añadir allowlist de extensiones.
Crítico · 1 día
🚫
Eliminar subprocess con shell=True en workers/export.py
Reemplazar subprocess.run(cmd, shell=True) por lista de argumentos: subprocess.run([bin, arg1, arg2]). Nunca pasar input del usuario a comandos de shell.
Crítico · 1 día
🔄
Reemplazar pickle por msgpack o JSON en redis_cache.py
Sustituir pickle.dumps/loads por msgpack.packb/unpackb o serialización JSON explícita. Añadir HMAC de integridad sobre los datos en caché.
Alto · 3 días
🔑
Migrar JWT a RS256 con clave de 2048 bits (o Ed25519)
El secret HS256 actual (16 bytes) es insuficiente. Adoptar RSA-2048 o Ed25519 con almacenamiento en variable de entorno o secretos Vault. Rotar tokens activos tras el cambio.
Alto · 1 semana