4.9 KiB
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.
- Confirm receipt and assign a Django ticket (code.djangoproject.com).
- Assess severity — django-0001 fires on every deferred queryset iteration; django-0003 fires at every server startup.
- Coordinate a disclosure date — we are targeting 90 days from first contact.
- 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.