np_auditor ← volver
Auditoría · Django · Fase E
1 de agosto, 2026 · Fase E v2 · shallow scan

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.

Léelo así

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.

Para mortales

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.

23Capa1 conocidos
462Cuarentena
400Match objetivo
62Candidatos nuevos

Tabla de métricas

django · Fase E v2 · ts 2026-08-01T21:48:39Z
MétricaValor
Repo / orgdjango/django · Django Software Foundation
Branch / SHAmain · 60121939
Scan pathscore · db · http · utils · views · forms · ≈319 .py · 8.5 s
Capa1 conocidos23
Cuarentena / match / nuevos462 / 400 / 62
Oráculo pass / fail0 / 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

Veredicto del auditor

En una frase

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

01
Capa1 — conocidos 23 hits con CWE. El más claro: deserializacion_pickle_insegura en backends de cache.
02
Cuarentena 462 tracks sin CWE inventado. Volumen alto = mucho sink AST en un framework grande.
03
Match de objetivos 400 matches — pero ~180 son obj_sqli_concat sobre cursor.execute (mucho FP de familia).

Detalle técnico — hallazgos del JSON

CWE-502 django/core/cache/backends/filebased.py:38 · pickle.loads Capa1
if not self._is_expired(f):
    return pickle.loads(zlib.decompress(f.read()))
También locmem.py, redis.py y en cuarentena db.py:102 (pickle.loadsobj_pickle_rce 0.9). Señal correcta del motor; exploitabilidad depende de quién escribe el store de cache.
CWE-89 django/db/backends/base/creation.py:215 · sql_armado_por_interpolacion Capa1
También Oracle creation.py:336. Contexto de tooling / creación de DB, no el request path típico de una app.
obj_sqli_concat django/core/cache/backends/db.py:75 · cursor.execute Ruido heurístico
with connection.cursor() as cursor:
    cursor.execute(
        "SELECT %s, %s, %s FROM %s WHERE %s IN (%s)"
        % (quote_name("cache_key"), …)
180 matches de 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.
obj_cmd_injection django/utils/version.py:93 · shell=True Capa2 match 0.9
git_log = subprocess.run(
    "git log --pretty=format:%ct --quiet -1 HEAD",
    capture_output=True,
    shell=True,
    cwd=repo_dir,
Git log para timestamp de versión en desarrollo — shell=True real, superficie acotada.
obj_rce_eval django/db/… · compiler.compile Nombre engañoso
Hits altos en operations.py, aggregates.py, expressions.py: es el SQL compiler de Django (compiler.compile(value)), no compile() de bytecode.
obj_mass_assignment django/…/color.py:97 · setattr Ruido típico
setattr en utilidades de color / backends → obj_mass_assignment. Heurística floja fuera de modelos web.

Cómo se validaría bien (defensivo)

Sin explotación de terceros

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