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
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 validarapi/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 SQLapi/routes/search.py:89
⚠
Sink — ejecución en base de datos PostgreSQL
asyncpg.Connection.execute() — ejecución directa de SQL arbitrarioapi/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") # ← SOURCE82 if not search_term:83 raise HTTPException(status_code=400)8485 # TODO: migrate to ORM — deuda técnica desde sprint 386 # WARNING: raw SQL temporal hasta que se mapee el modelo completo87 raw_sql = f"SELECT * FROM products WHERE name LIKE '%{search_term}%'" # ← TAINT88 results = await db.execute(raw_sql)89 return [dict(row) for row in results.fetchall()] # ← SINK9091 # 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