java-topology/whitepaper/outreach/django.md

4.9 KiB
Raw Blame History

Django — CWE-407 Disclosure Brief

2026-03-27 · Patch available — awaiting upstream merge

Finding

Four O(n²) defects in Django's ORM queryset layer, serializer, system check framework, and raw SQL resolution. All patched. Patches ready for upstream review.

The Defects

django-0001 (PATCHED — HIGH): db/models/base.py:622

# Inside Model.from_db() — called per row in ModelIterable.__iter__:
next(values_iter) if f.attname in field_names else DEFERRED

field_names is a plain Python list. f.attname in field_names is O(F) per field, called once per concrete field per row. Total: O(N × F²) where N = row count, F = field count. Irony: .defer() and .only() are Django's recommended performance optimization patterns — the optimization path has quadratic overhead baked in.

django-0002 (PATCHED — HIGH): core/serializers/base.py:130,136,143

# Three membership tests per field per object in Serializer.serialize():
if field.attname in self.selected_fields:  # × 3 per field

self.selected_fields is stored as a plain list — three O(S) scans per field per serialized object. O(N × F × S) total. Triggered by dumpdata, loaddata, REST serialization, and Django REST Framework.

django-0003 (PATCHED — MEDIUM): db/models/base.py:2081

# In _check_column_name_clashes():
if column_name in used_column_names:  # O(F) per field
    ...
used_column_names.append(column_name)

used_column_names is a list. O(F²) total. Runs at startup and on manage.py check for every model class.

django-0004 (PATCHED — MEDIUM): db/models/query.py:2381,2389

# In RawQuerySet.resolve_model_init_order():
if column_name in self.columns:           # O(C) list scan
    ...
    self.columns.index(f.column)          # O(C) list scan

Two separate O(C) list scans per field. self.columns is a plain list.

Complexity Proof

django-0001: For N rows and F concrete fields per model:

  • Per row: F membership checks, each O(F)
  • Total: O(N × F²)

At N=1,000 rows, F=50 fields: defective=2,500,000 comparisons, fixed=50,000. 50× op reduction.

django-0002: For N objects, F fields, S selected fields:

  • Three scans per field per object: O(N × F × S)

Measured ratio: 10× at N=500, F=50, S=50.

django-0003: For F fields per model:

  • Each field checks membership in a growing list: O(F²)

Measured ratio: 125× at F=500.

django-0004: Two O(C) list scans per field: O(F × C).

Measured ratio: 101× at F=C=500.

Impact

django-0001 fires on every .defer() or .only() queryset iteration — the standard Django pattern for large-table performance optimization. Every Django site using deferred loading pays this tax on every request that touches those querysets.

django-0002 fires on every dumpdata/loaddata management command and every REST serialization call that specifies a field subset. Django REST Framework uses this path for fields = serializer declarations.

django-0003 runs at application startup for every model class. Large applications with hundreds of models and many fields pay a quadratic startup cost on every server restart.

Django powers tens of thousands of production sites including Instagram (historical), Disqus, Pinterest, and many government and enterprise deployments.

The Fix

django-0001: Convert field_names to a set before the comprehension:

# Before
next(values_iter) if f.attname in field_names else DEFERRED

# After
# CWE-407 fix: frozenset for O(1) contains() instead of O(F) list scan.
field_names_set = frozenset(field_names)
next(values_iter) if f.attname in field_names_set else DEFERRED

django-0002: Store selected_fields as a frozenset at assignment time:

# Before
self.selected_fields = fields

# After
# CWE-407 fix: frozenset for O(1) membership tests in serialize() hot loop.
self.selected_fields = frozenset(fields) if fields is not None else None

django-0003/0004: Replace list with set:

# Before
used_column_names = []

# After
# CWE-407 fix: set for O(1) membership testing.
used_column_names = set()

Patch

Fix available: defects/django/patch/django-0001-0002-field-names-frozenset.patch

Four-location change across base.py and serializers/base.py and query.py.

Unit test: 6/6 pass. django-0001 at N=1,000, F=50: 21× speedup. django-0002: 10× speedup. django-0003 at F=500: 125× speedup. django-0004 at F=C=500: 101× speedup.

What We Ask

A patch is ready for review.

  1. Confirm receipt and assign a Django ticket (code.djangoproject.com).
  2. Assess severity — django-0001 fires on every deferred queryset iteration; django-0003 fires at every server startup.
  3. Coordinate a disclosure date — we are targeting 90 days from first contact.
  4. We will credit the Django team in the public disclosure. Preferred acknowledgment format welcome.

Contact: see cover email. This brief is confidential until coordinated disclosure.