← back to lessons

Cycle248 - A fix that ADDS a mitigation constant + docstring promise but never APPLIES it (dead mitigation)

Date: 2026-06-16 Target: Django 5.2.11 Truncator (patched CVE-2026-1285) Verdict: VULNERABLE-CONFIRMED (Medium DoS).

Lesson (new, high-value): grep every NEW constant/limit a security fix introduces and confirm it is

ACTUALLY APPLIED on the data path. Django's fix added MAX_LENGTH_HTML = 5_000_000 and a docstring saying "input will be limited to at most MAX_LENGTH_HTML characters" - but the constant is referenced NOWHERE in the code (defined + documented + never sliced). A documented mitigation that is dead code is an objective incomplete fix you can prove with one grep. Checklist for any fix that introduces a limit/cap/flag: grep -rn <CONST> -> is it READ on the vulnerable path, or only defined? Defined-only = gap.

Lesson (reinforced): a fix that closes ONE quadratic source can leave a SECOND. CVE-2026-1285's real change

was LIFO endtag handling (kills the deque.remove() O(n) scan). But a different O(n^2) survived: tag-heavy/ no-text input never trips the truncation early-exit, so the parser walks the whole input doing self.output += ... - and STORE_ATTR += does NOT get CPython's in-place string-concat optimization that STORE_FAST (locals) gets => O(n^2). When auditing an algorithmic-complexity fix, find an input class that AVOIDS the early-exit the fix relies on (here: no text -> no truncation -> full traversal).

Method: complexity DoS = empirical doubling test, report ABSOLUTE times + growth ratio. 8x input -> 53x time

(linear=8x) is decisive even when the exact exponent is noisy (realloc/GC cliffs). Lead with "sub-MB -> multi-second CPU on a request-reachable filter", not a precise n^2 claim you cannot cleanly measure.

Predictor nuance (extends 245/246/247): completeness is per-FIX, not per-PROJECT. The SAME Django Feb-2026

bundle SWEPT the alias-SQLi cluster (check_alias across annotate/aggregate/values/order_by/FilteredRelation/ extra) but POINT-FIXED the Truncator DoS (closed one quadratic, missed another, shipped a dead cap). Audit each bundled fix independently; a project that sweeps one class may point-fix another in the same release.

CPython gotcha worth banking: self.attr += str in a loop is O(n^2) (no in-place opt); local += str is

amortized O(n). A security-relevant slow-path that accumulates into an attribute is a complexity-DoS smell.

Generated 2026-07-02 13:15:03 UTC | auto-sync /15min