v1.49.0
secure-code-auditor v1.49.0
The final checkpoint on the five hardening releases. One deterministic sweep of
the whole repository, seventeen tests, and every failure fixed in the same
session. No reference file is added, no heading is renamed, and the reference
count stays at twenty-four.
The ledger holds on both sides. All thirty accepted items from v1.44.0 through
v1.48.0 are still present. Every rejected item is still absent: no --strict
flag, no --fail-on, no CHANGELOG.md, no tests/, the six labelled
wrong-versus-correct pairs in the DRF file unchanged from the v1.43.0 count, and
the exit-code contract standing as written.
Corrections
Each was re-verified against a primary source fetched on 20 August 2026.
- The gevent floor was paired with the wrong CVE. CVE-2023-41419 is first
patched in 23.9.0, not 24.10.1. GHSA-x7m3-jprg-wc5g gives the range as
< 23.9.0, and the upstream issue body says "Fixed in 23.9.0". This one had
been flagged for re-verification since the 13 August audit and is now settled.
Theeventlet>=0.40.3andtornado>=6.5.0floors beside it re-verified as
correct. - The connection-pool claim was false, and it was the strongest claim in its
section.data-layer-and-database.mdsaid a pool's maximum size bounds
total backends "regardless of how many workers exist". Django holds
DatabaseWrapper._connection_poolsas a class attribute, so every process
builds its own pool:max_size: 10across eight workers on three hosts is 240
backends. The passage now multiplies by processes and by hosts, credits only
PgBouncer with capping from outside the processes, and the sizing bullet three
lines down gains the host factor so the two agree. PersistentRemoteUserMiddlewaredoes read the header. A07 said it
"returns without reading the header at all".process_request()calls
get_username()and catchesKeyError;force_logout_if_no_headeris
consulted only then. The flag decides the logout, not the read.- The 6.1
checkchange is attributed to the wrong place. The command still
passesoptions["databases"], which isNone. The default lands inside
run_checks(), which 6.1 gave both anelif not databasesfilter and a
databases is Nonefallback that 6.0 has neither of. - The tar filter backport list was incomplete.
Misc/NEWS.d/3.8.17.rst
carries the same entry, so 3.8.17 joins 3.9.17, 3.10.12, and 3.11.4. - A08 contradicted itself. Its "Django 6.1 was not available to check"
caveat sat in the same section as the 6.1 items v1.48.0 added. It now says the
6.1 items name their release and an unnamed claim is scoped to 6.0.
Controls added
Nine, each at sentence scale in the file the ownership table makes the owner.
- A01 pins the resolved address for SSRF. "After DNS resolution" left a
re-resolving check reading as safe, and DNS rebinding defeats it. - The default-deny URLconf test gets a closed exemption set. A prefix tuple
also exempts every route added below it later, which defeats the test's own
purpose. - A07 flags that the
authtokenTokenmodel stores its key as plain text. - A09 requires the edge to replace a client-supplied
X-Request-ID, so a
forged identifier cannot splice one principal's trace into another's. - A10 names
btree_giston theExclusionConstraintexample, which the
scalar-beside-a-range booking case needs. - api-drf names
DATA_UPLOAD_MAX_MEMORY_SIZEwhere a webhook reads
request.bodydirectly, since that passage is about bypassing DRF's parsers. - async-and-channels makes the WebSocket ticket single-use. The same passage
warns that URLs reach logs, and the ticket rides in the URL. - file-uploads pins the
versionIdor generation at promotion.
Write-denying the served prefix does not stop a re-PUT into quarantine between
the clean verdict and the copy.
Attack-pass items that passed
Six of the fifteen passed and are recorded as passes rather than written into
findings. The KDF-input length ceiling is the one worth naming: Django added a
4096-byte cap in 1.4.8 and reverted it in 1.5.5, AuthenticationForm.password
carries no max_length, and OWASP declines to recommend a general maximum. The
bcrypt 72-byte truncation in A04 and the 64-character floor in A07 are the
ceilings that belong here, and both were already correct.
Sibling non-absorption
a03-software-supply-chain.md carried migration operation mechanics — pending-
trigger errors, atomic = False, restart-safe predicates, RunPython.noop —
with no handoff. That is django-migration-safety's deliverable. It is cut to
the security invariant, that the access rule holds at every batch boundary, plus
the handoff by name, matching the shape A08 already uses for
django-async-jobs.
Language and register
Twenty-six sentences this build added exceeded the ASD-STE100 length limits or
carried two instructions in one sentence, and are split. Five uppercase
MUST/NEVER uses are lowered: the corpus carries none, using sentence-case
"Never" and lowercase "must" throughout, so the uppercase form was an import
rather than a match.
Budgets
a04-cryptographic-failures.md goes from 1119 to 1112 lines, closing the
deferral v1.47.0 opened and v1.48.0 carried. A mechanical duplication pass,
internal and cross-file, found three passages restating a file that owns them:
the disk-versus-column distinction and the encrypted-column query cost belong to
data-layer-and-database.md, and the os.environ rule to
service-identity-and-secrets.md, whose version is stricter because it also
rules out os.environ.get. Those are pointers now. The rest of the file is
controls rather than duplication, so it stays over the 1100 convention
deliberately — trimming further would delete controls, and this release closes
the item rather than deferring it a fourth time.
SKILL.md is 33,714 of its 40,960-byte ceiling. The description is untouched at
1013 characters.
Verification
Every Django 6.1 claim this build wrote was re-verified against the 6.1 release
notes or the tagged source; none failed. CVE-2026-6873 verified against the NVD
record. All eighty-one identifiers the build introduced resolve in the Django,
DRF, or Python documentation, or in the named project's own source. The
self-test holds 49 fixtures and passes, all three scanners end a --json stream
on a summary record and exit 0 on a missing path, all three produce zero
findings against this repository, and every intra-repository link and heading
anchor resolves. The library index date does not move.
Community health files are added in a separate commit and carry no version:
CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md, four issue forms, and a
pull-request template. Issues yes, pull requests no.
Full Changelog: v1.48.0...v1.49.0