Skip to content

v1.48.0

Choose a tag to compare

@n-shadloo n-shadloo released this 20 Aug 18:39
· 36 commits to main since this release
v1.48.0
a93b033

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 under RemoteUserMiddleware, whose force_logout_if_no_header is
    True — and open under PersistentRemoteUserMiddleware, which sets it to
    False and 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 of None preserves 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 SELECT aliasesa05-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__, the File truthiness change whose upload
    subclasses keep the old behavior, and the task() 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