v1.45.0
secure-code-auditor v1.45.0
A correction release. Every verified factual error and every defective example
in the reference corpus is fixed, each against its primary source. Fifteen
reference files change, two controls are new, and no reference file is added,
no heading renamed, and no script touched.
Each Django and DRF claim behind these corrections was read off installed
source on 20 August 2026 — Django 6.0.7 and DRF 3.17.1 — and each PostgreSQL,
Celery, PyJWT, and Have I Been Pwned claim was checked against that project's
own documentation. The library index date does not move: no PyPI sweep ran for
this release.
The corrections that change a verdict
Stock DjangoObjectPermissions does not check safe methods. Its
perms_map holds an empty permission list for GET, HEAD, and OPTIONS,
and has_perms over an empty list is True — so a safe request makes no
object check, and the documented "safe method, permission missing → 404" row
could never fire on the stock class. authorization-architecture.md now states
that read access is view-level only until the map is subclassed, supplies the
subclass, and scopes the 404/403/404 matrix to it. The instruction not to
"fix" the 404 into a 403 is unchanged; it was always right for the subclassed
form.
Four errors in data-layer-and-database.md. pg_dump sets row_security
to off and a restricted role gets an error rather than a silently partial dump
— --enable-row-security is the opt-in that filters, so the incompleteness
runs the opposite way from what the file said. An ALL or UPDATE policy with
no WITH CHECK reuses USING as its check, so the real trap is a FOR SELECT
policy beside a separate permissive write policy, combined with OR.
REPEATABLE READ is snapshot isolation that permits write skew across rows;
only SERIALIZABLE detects it. And the alias on transaction.atomic() does
not route the ORM, which the retry example now says at the call it wraps.
AUTH_PASSWORD_VALIDATORS never ran on the paths that need them most. They
run where validate_password() is called: the built-in auth forms call it,
while set_password(), the model constructor, and create_user() do not.
a07-authentication-failures.md now says so, and points the custom
UserManager example at the rule. The Have I Been Pwned Add-Padding header
varies the entry count inside a band rather than making responses byte-uniform,
and the outbound read is capped.
Examples that taught the defect they warn about
Three blocks labelled "correct" carried one.
The DRF serializer bound extra_kwargs to a password its fields list
omitted, so the write_only rule it demonstrated applied to nothing. It now
declares the field and validates the secret before hashing it.
The async membership transfer authorized the source tenant and then wrote the
destination — which lets an admin move a member into any tenant by identifier.
Both sides are now loaded and authorized inside the one transaction.
The webhook receiver acknowledged an event it could then lose: a crash or a
broker outage between the row commit and the enqueue left the provider
satisfied and the work gone. It now stores a RECEIVED record and enqueues
inside transaction.on_commit, with the sweep that recovers a lost wake-up
named. The delivery mechanics stay with django-async-jobs; this file owns
only that the verified event survives the acknowledgment.
Two new controls, both already scanned
Writable relation fields. ModelSerializer builds each writable
PrimaryKeyRelatedField with the related model's default queryset, so the
client may name any row in the table — another tenant's included. The same rule
reaches SlugRelatedField, nested writes, and the validator querysets, where an
unscoped UniqueValidator is an existence oracle across tenants.
ModelForm and formset mass assignment. The serializer failure has a
non-DRF twin that CFG001 already fires on and no file owned:
Meta.fields = "__all__" on a form, exclude failing open the same way, a
server-owned field rendered as a hidden input, and the formset bounds
(absolute_max, validate_max, DATA_UPLOAD_MAX_NUMBER_FIELDS) that a
project can raise into a caller-controlled work multiplier.
Smaller corrections
At-least-once delivery begins at the broker, and the enqueue itself is lost
when the process dies before the callback (A10). The idempotency fingerprint
needs the method and the route beside the body (A10). The two debug decorators
reach Django's own error reports only, and extra= populates the record while
the formatter decides the output (A09). SECRET_KEY rotation needs two phases
on a rolling fleet, because one deploy leaves instances signing with a key
their neighbours cannot validate. RFC 8693 exchange sends the subject token,
not decoded claims. PyJWT verifies HMAC only without the crypto extra. Django
6.2 is unreleased, so listurls takes a future tense. The MultiFernet
rotation pass overwrites a row the application changed mid-batch. A nested
router adds no predicate without a parent-filter mixin, and the parent object
still needs its own authorization. safe_join's containment is lexical, so a
writable storage tree defeats it between the check and the open.
Django's YAML fixture serializer loads with SafeLoader, so a YAML fixture is
not the yaml.load finding — its risk is the same unvalidated bulk write as
every other format. The unsafe-YAML bullet is realigned to the scanner's
calibration, and result_accept_content is named beside the Celery pair it
belongs with.
Adjudicated, no edit
The raw-SQL section already states that a raw path bypasses the row-level
security context, and the command example already scopes -- with "where the
program supports it". Both were checked and left alone.
The description is unchanged at 1013 of 1024 characters.
Full Changelog: v1.44.0...v1.45.0