v1.48.0
secure-code-auditor v1.48.0
A Django 6.1 release. Django 6.1 shipped on 5 August 2026, and the corpus was
written against 6.0. Twelve security-relevant behavior changes now sit inside
the passage that owns each one. Eight reference files change, no reference file
is added, no heading is renamed, and no script is touched.
Every item below was verified on 20 August 2026 against the 6.1 release notes
read end to end, the linked 6.1 documentation, or the Django source at the 5.2,
6.0, and 6.1 tags. The change brief that prompted this release was treated as an
impetus and not as a source: several of its candidates were dropped, and three
survived only in a narrower or wider form than the release notes read. Those
three are called out below. The library index date does not move — this was a
framework release, not a package sweep.
Password hashing and signing
a04-cryptographic-failures.md carries the new PBKDF2 default. The shipped
iteration count is 1,500,000 on 6.1, against 1,200,000 on 6.0 and 1,000,000
on 5.2, read off django/contrib/auth/hashers.py at each tag. Argon2 and Scrypt
keep their parameters on 6.1 and the shipped PASSWORD_HASHERS order is
unchanged, so the rest of that table re-scopes as it stands rather than needing
a 6.1 row of its own.
The signing notes take the 6.1 deprecation of the implicit algorithm default
on salted_hmac() and django.core.signing.base64_hmac(). Both now warn until
the caller names the algorithm, ahead of the 7.0 change to "sha256" the file
already recorded.
RemoteUserMiddleware under ASGI
This is the first of the three items that came out subtler than the release note
reads. The note says the HTTP_ prefix is no longer added under ASGI, and that
the REMOTE_USER default is unaffected. The 6.1 source says why: get_username()
adds the prefix only when the request is an ASGIRequest and header still
equals the base-class value. So the default goes on reading HTTP_REMOTE_USER,
while a custom header is read exactly as written and has to be spelled
HTTP_AUTHUSER. Django 5.2 and 6.0 prefixed unconditionally on the async path,
so one subclass resolved two different keys depending on the server.
Two upgrade hazards follow, and both are silent:
- A custom-header subclass reads a key nothing populates on 6.1. That fails
closed underRemoteUserMiddleware, whoseforce_logout_if_no_headeris
True— and open underPersistentRemoteUserMiddleware, which sets it to
Falseand therefore returns without consulting the header at all. The session
then outlives any revocation at the proxy. - Django 6.1 removed the 6.0 shim that ran a
process_request()override through
sync_to_async. Such an override is now skipped under ASGI while the base class
still authenticates, so any check it added stops running.
a07-authentication-failures.md also now states the ASGI rule the file never
carried: Django's own documentation says the spoofing warning applies under ASGI
in every configuration, because an ASGI server cannot place a trusted value in
the environ.
Redirects that preserve the request
a01-broken-access-control.md gains RedirectView.preserve_request, new in 6.1.
The preserve_request argument on HttpResponseRedirect and redirect() is
not new — it is 5.2 — and the passage scopes each to the release that added
it. An enabled one rates above a plain open redirect, because a 307 or 308 sends
the POST body to the target host and not only the visit.
The second narrowed item is the redirect length bound. It is not new either:
Django 6.0 hard-codes 16384 characters and raises DisallowedRedirect above it.
What 6.1 adds is the max_length argument that overrides it, so max_length=None
now removes a bound the framework used to enforce for every project.
CSP nonces, mailers, and check --deploy
a02-security-misconfiguration.md takes three changes.
The CSP passage gains the nonce path. security.W027 reports CSP.NONCE in a
policy with no django.template.context_processors.csp context processor — a
second way a policy goes inert, after the missing middleware the bundled scanner
already flags. Without the processor the header promises a nonce that no element
carries, so every inline script the policy was written for is blocked. The new
csp_nonce_attr tag renders the attribute on external <script> and <link>
elements and on a Media object's assets.
The mail material gains MAILERS, which moves the backend, the credentials, and
the TLS posture into a per-alias dictionary and deprecates eleven EMAIL_*
settings. Review each alias on its own: one mailer can hold TLS while a second
sends in cleartext, and neither setting contradicts the other. The mail.E001
deploy check rejects a development-only backend in the default alias, and
mail.W001 reports a MAILERS value with no default entry.
The third narrowed item is the check command. The release note says it now
supplies all databases; the 6.1 run_checks() source says it defaults
databases to every configured alias but still filters database-tagged
checks out without an explicit --database. Django's own deploy checks
therefore still read settings alone. The real exposure is a custom or
third-party check that accepts databases and opens a connection in CI.
Database-level cascades and the audit trail
a09-logging-and-alerting.md takes the delete-signal half of DB_CASCADE.
SKIP_COLLECTION in 6.1's django/db/models/deletion.py holds DO_NOTHING,
DB_CASCADE, DB_SET_DEFAULT, and DB_SET_NULL. An audit hook on a delete
signal therefore records nothing for the far side of such a relation, and the
diff that causes it is one keyword long.
The erasure half was already correct in data-lifecycle-and-privacy.md and is
left alone. What was missing is the audit half, because a09 stated that
QuerySet.delete() sends delete signals for cascades without exception.
Request parsing and strict Base64
file-uploads.md gains HttpRequest.multipart_parser_class, which selects the
parser that reads the request body — a replacement owns every bound above it,
because the settings are read by the parser rather than applied around it. It
also gains the strict Base64 validation now applied by MultiPartParser,
BinaryField, and DatabaseCache. Each is a fail-closed change, so the handler
around those paths should return a 400 rather than a 500.
The ownership call: request-body parsing belongs here with the upload pipeline,
and api-drf-specific.md keeps DRF parser configuration only.
Admin actions and the permission model
authorization-architecture.md takes the admin action location argument.
ActionLocation.CHANGE_FORM can place an action on the change form, which gives
an action declaring no permissions a second unguarded entry point past
_filter_actions_by_permissions() — the gap this file recorded at v1.47.0. The
same change deprecates the get_actions() signature the file's own mitigation
depends on, and its return values are Action objects now, so that sentence is
updated with it.
The access-review section takes two more: a migration that renames a model now
renames the matching Permission.name and Permission.codename, and the new
Permission.user_perm_str returns the string User.has_perm() expects.
Deserialization
a08-integrity-and-deserialization.md takes the XML deserializer raising
SuspiciousOperation on an unexpected nested tag, and the picklability of Task
and TaskResult. Task.__reduce__ stores module_path, and Django resolves it
through import_string when it rebuilds the object — the same import-driven path
the file already records for TaskError.exception_class.
Neither fact is a new risk in a pickle store an attacker can already write, since
pickle runs code by design. What changes is that a TaskResult holding arguments
and tracebacks verbatim can now reach a pickle-backed cache or session, so the
retention and scrubbing rules the file already states reach further.
Dropped after checking
delete_confirmation_max_display— its own documentation calls it purely a
display setting that does not change what is retrieved from the database, and
the default ofNonepreserves the old behavior.- The admin login redirect to
next— it resolves through
LoginView.get_redirect_url(), which validates with
url_has_allowed_host_and_scheme. - Systematic quoting of SQL
SELECTaliases —a05-injection.md's alias
section already generalizes framework hardening across versions and keeps the
durable fix. Inviting a reader to lean on the quoting is the wrong lesson in
the one section built to prevent exactly that. SessionBase.__bool__, theFiletruthiness change whose upload
subclasses keep the old behavior, and thetask()decorator's new keyword
arguments — extensibility or no security behavior.- Dropped PostgreSQL 14, MySQL < 8.4, MariaDB < 10.11, and SQLite < 3.37
support — a support matter with no home in this corpus. - Fetch modes and the
select_related()deprecation — these belong to
django-performance-optimizer.
Already landed
The signed-cookie item needed no edit. a02-security-misconfiguration.md
already carries CVE-2026-6873, the 5.2.15 and 6.0.6 floor, the 6.1 flip of
SIGNED_COOKIE_LEGACY_SALT_FALLBACK to False, and the removal at 7.0.
Known carry-over
a04-cryptographic-failures.md is now 1119 lines and stays over the 1100-line
convention that v1.47.0 deferred to the final checkpoint.
Full Changelog: v1.47.0...v1.48.0