Django: pickle, SQL y el pedo del ruido heurístico
Sobre subdirs del core de Django: 23 Capa1 (pickle en cache), 462 cuarentena, 400 match. Señal útil y FP de familia, sin humo.
Esto no es “hackeamos Django”. Es SAST + cuarentena sobre subdirs del core. Pickle en cache es un patrón documentado; muchos matches SQL son ruido de familia. Candidato estático ≠ CVE confirmada.
Qué miramos: core de Django (core, db, http,
utils, views, forms — ≈319 .py) en
main (60121939…), solo lectura AST.
Qué salió: 23 conocidos Capa1 (destaca pickle / CWE-502), 462 en cuarentena,
400 match, 62 nuevos. Oráculo 0/0.
Qué significa: el framework usa patrones que un SAST debe ver
(deserialización, SQL helpers, templates, shell en tooling).
Qué NO significa: que Django esté “roto” en producción ni 400 bugs explotables.
Tabla de métricas
| Métrica | Valor |
|---|---|
| Repo / org | django/django · Django Software Foundation |
| Branch / SHA | main · 60121939 |
| Scan paths | core · db · http · utils · views · forms · ≈319 .py · 8.5 s |
| Capa1 conocidos | 23 |
| Cuarentena / match / nuevos | 462 / 400 / 62 |
| Oráculo pass / fail | 0 / 0 |
| Top objetivos (match) | sqli_concat 180 · mass_assignment 87 · rce_eval 85 · ssti 19 · cmd_injection 9 |
| CWE Capa1 (top) | CWE-502 ×6 · CWE-347 ×5 · CWE-829 ×2 · CWE-89 ×2 |
Auditoría de impacto (lo que sí huele fuerte)
En Django el volumen miente: ~180 obj_sqli_concat no son 180 SQLis.
Lo que huele de verdad son trust boundaries de cache, management commands y deserialización.
1. Pickle en backends de cache — CWE-502 / obj_pickle_rce
Ancla
Capa1 ×6: filebased.py:38/71/154, locmem.py:43/73, redis.py:29 (deserializacion_pickle_insegura). Capa2: cache/backends/db.py:102 · pickle.loads.
Qué pasa
Django cache serializa valores con pickle. Al leer, loads reconstruye objetos Python arbitrarios — clase canónica de RCE vía deserialización si el blob no es de confianza.
Qué se puede hacer / cómo podría fallar
Impacto: ejecución de código en el proceso del worker si un atacante escribe en el store de cache (Redis/Memcached/FS compartido sin auth, o path de filebased alcanzable). Precondiciones: (1) control del blob en el backend, (2) la app usa ese backend, (3) no hay MAC/firma del valor. En locmem el blast radius es proceso-local.
Cómo podría ser explotado (abstracto)
Cadena: compromiso o escritura en cache compartida → request que hace cache.get → pickle.loads → gadget RCE. Django documenta que el cache debe ser de confianza; el hallazgo confirma que el sink existe. Falta oráculo propio + modelo de amenaza del deploy (¿Redis expuesto?).
2. eval en migrations questioner — CWE-95
Ancla
db/migrations/questioner.py:168 · eval_sobre_datos_externos.
Qué pasa
El interactive questioner de migraciones puede evaluar defaults tipados desde input del desarrollador en consola.
Qué se puede hacer / cómo podría fallar
Clase code injection en contexto de management command interactivo. Precondiciones: alguien pega/ejecuta el comando y suministra el default; no es un endpoint HTTP anónimo. Impacto = la máquina del dev/CI que corre makemigrations.
Cómo podría ser explotado (abstracto)
Escenario: social engineering / script de onboarding que alimenta stdin, o CI que pipea defaults no confiables. No es “RCE remoto de Django en prod” sin esa precondición. Aún no confirmado explotable en automatización real sin traza de input.
3. SQL armado en creation de test DB — CWE-89
Ancla
db/backends/base/creation.py:215, oracle/creation.py:336 · sql_armado_por_interpolacion.
Qué pasa
Creación de bases de prueba interpola identificadores/SQL. Es superficie de SQL injection si nombres o fragmentos vienen de fuera.
Qué se puede hacer / cómo podría fallar
Impacto típico: abuso en tooling de tests (crear/drop DB), no el request path de una view. Precondiciones: control del nombre de DB/usuario de test o de statements auxiliares. Distinto de los cientos de cursor.execute parametrizados del ORM.
Cómo podría ser explotado (abstracto)
Cadena: config de test hostil → comando de creación → SQL con identificadores peligrosos. Verificar quoting de identificadores vs valores; no tratar todo cursor.execute como SQLi.
4. Shell / subprocess en tooling — obj_cmd_injection
Ancla
management/commands/shell.py:272 (exec de stdin); db/backends/base/client.py:31 (subprocess.run); mysql/creation.py:81 (Popen dump); utils/version.py:93 (shell=True para git log).
Qué pasa
Management y backends lanzan procesos o ejecutan código para el desarrollador. django-admin shell que hace exec de stdin es por diseño un REPL.
Qué se puede hacer / cómo podría fallar
Command injection si args de dump/client mezclan input no confiable con shell; o abuse de shell si un pipeline CI pipea código a shell. Precondiciones fuertes de “quién corre el comando”.
Cómo podría ser explotado (abstracto)
No inventes CVE de “Django RCE web” desde estos sinks. El threat model correcto es: supply-chain / CI / operador local. Checklist: argv lista vs string; origen de repo_dir en version.py.
5. Templates / archives / HTML — SSTI, zip, XSS
Ancla
CWE-22 management/templates.py:320; archive.extract candidato en templates/utils; CWE-79 utils/text.py:128; matches template.render (defaults, management templates).
Qué pasa
startproject/startapp descarga y extrae plantillas; views de error renderizan templates; helpers HTML arman markup.
Impacto abstracto
Zip slip / path traversal si el archive de plantilla es hostil; SSTI solo si el string del template (no el contexto) es atacante-controlado — en Django el engine usual escapa contexto, distinto de Jinja sandboxed mal; XSS reflejado/stored si HTML se arma sin escape hacia respuesta. Falta taint de “quién controla el template string”.
Lo que no conocemos (banco / cuarentena sin etiqueta fina)
62 candidato_nuevo sin etiqueta fina. Patrones dominantes:
Grupo A — *.render / t.render / response.render
Sospecha SSTI genérica. En Django, response.render() y widget render son el ciclo normal de TemplateResponse — casi siempre ruido. Sospechoso de verdad: Engine().from_string(user_controlled). Checklist: ¿el template string viene de disco/settings o de request?
Grupo B — *.decode (query_string, content, value, signing)
Boundary ASGI/WSGI/mail. Podría alimentar header injection / email injection si el string va a cabeceras sin sanitizar. Solo decode → ruido. Checklist: un hop de taint hacia HttpResponse headers o SMTP.
Grupo C — re.search / sqlparse.parse / memcached key regex
Parsers y validadores (i18n.py, introspection MySQL/SQLite, memcached_error_chars_re). Sospecha ReDoS o “parse = malo”. Probable ruido salvo regex con cuantificadores anidados sobre input HTTP. Checklist: origen del string + complejidad del patrón.
Grupo D — archive.extract (candidato)
utils/archive.py:52, management/templates.py:377. Aquí sí hay hipótesis de zip slip / path traversal alineada con CWE-22 del download. Checklist: ¿normalizan nombres antes de escribir? Fixture propia con entradas ../ en harness — sin tocar el repo ajeno en ataque.
Ruido vs señal
- Descartaría en masa: ~180
cursor.execute/self.execute→obj_sqli_concatcuando el ORM usa placeholders; el sink existe, la vulnerabilidad no se infiere del nombre. - Descartaría:
compiler.compile→obj_rce_eval(compilador SQL de Django, nocompile()de bytecode). - Descartaría:
setattr×87 mass_assignment; CWE-347 “firma” en migrations utils (heurística de nombre, no crypto). - Señal: pickle cache, eval questioner, SQL creation test DB, archive/templates paths, HTML en
text.py, cookie Secure (CWE-614) como higiene deploy. - Límite: sin contrib/, oráculo 0, shallow.
Veredicto del auditor
- P0 Modelo de amenaza de cache: ¿quién puede escribir blobs que Django deserializa con pickle? (Redis/FS compartido).
- P1 Flujo startproject/templates: download +
archive.extract+ contención de paths. - P1 Separar en el producto familia SQL parametrizada vs interpolación real (hoy infla matches).
- P2 eval questioner (dev-only), cookie Secure, lorem random, candidatos render/decode.
Django no “está lleno de SQLi”: está lleno de cursor.execute. La investigación seria empieza en pickle de cache y archives de plantillas.
Capa1 vs Capa2 / cuarentena
deserializacion_pickle_insegura en backends de cache.
obj_sqli_concat sobre cursor.execute (mucho FP de familia).
Detalle técnico — hallazgos del JSON
if not self._is_expired(f):
return pickle.loads(zlib.decompress(f.read()))
locmem.py, redis.py y en cuarentena
db.py:102 (pickle.loads → obj_pickle_rce 0.9).
Señal correcta del motor; exploitabilidad depende de quién escribe el store de cache.
creation.py:336. Contexto de tooling / creación de DB, no el request path típico de una app.with connection.cursor() as cursor:
cursor.execute(
"SELECT %s, %s, %s FROM %s WHERE %s IN (%s)"
% (quote_name("cache_key"), …)
obj_sqli_concat: muchos son cursor.execute con identificadores vía
quote_name, no concat cruda de input de usuario. El sink existe; la familia “concat” se pasa de lanza.
git_log = subprocess.run(
"git log --pretty=format:%ct --quiet -1 HEAD",
capture_output=True,
shell=True,
cwd=repo_dir,
shell=True real, superficie acotada.operations.py, aggregates.py, expressions.py:
es el SQL compiler de Django (compiler.compile(value)), no compile() de bytecode.
setattr en utilidades de color / backends → obj_mass_assignment. Heurística floja fuera de modelos web.Cómo se validaría bien (defensivo)
Fixture propia que aísle el patrón (p. ej. deserialización de cache con store controlado), oráculo Capa2 sobre esa fixture, y revisión humana del trust boundary documentado de Django. No se necesita atacar una instancia ajena ni fabricar PoCs ofensivos contra el proyecto.
Serie: caso agregado · Claude · aws-cli · Django · Metaflow · home