v1.52.0
secure-code-auditor v1.52.0
Two change sets land in one version. The first is the audit closure:
twenty-two commits, one per reference family, that repair what a file-by-file
audit of the whole corpus found — 3,992 insertions and 785 deletions across
twenty-three files. The second is the rebuild that follows it: the reference
corpus returns to the v1.50.0 language line the closure had crossed, and
SKILL.md and the agent cascade are rewritten to the same standard. No file
was added, renamed, or removed, and the frontmatter description is
byte-identical at 1013 characters.
The audit closure
Every reference took one commit, and the changes fall into three kinds.
Examples that taught the defect they warn about are corrected. The
canonical A01 CommentCreateView called check_object_permissions with
permission_classes = [IsAuthenticated] — BasePermission.has_object_permission
returns True, so the call authorized nothing; it now carries a permission
class that implements the object hook. The A04 wrapped-hasher migration gains
the survey pass that stops on a row it cannot wrap, because the write destroys
the only copy of the legacy digest, and the section now schedules the session
and reset-token invalidation that rewriting every password row causes. The
graphene UserType scope returns the set a caller may read rather than the
one row the caller is, and refuses an unauthenticated principal outright,
because AnonymousUser has no primary key and filter(owner_id=user.id)
compiles to owner_id IS NULL. The A09 service example records a permission
denial outside the transaction the denial rolls back, and the DRF atomic
counter chooses its cache-outage branch instead of letting the error become
a 500.
Decision rules that let real findings through are closed. The
duplicate-route dismissal now compares five things — the permission class,
the decorator, the middleware, the queryset scope, and the serializer — where
it compared three. The resolved-route artifact carries the request.urlconf
and set_urlconf caveat, because a middleware that sets either moves the
whole table. The tenancy ladder separates who authored a tenant value from
whether it is still true, and asks for a re-read of membership on the
request. The SSRF allowlist states how a host matches — equality on the
parsed, lowercased, IDNA form, with the scheme and port pinned and userinfo
rejected. A model field's validators argument no longer closes a sink
finding, because save(), bulk_create(), QuerySet.update(), migrations,
and raw SQL never run it.
Verified mechanics land across every file, each read off a primary source
and dated, most against the Django 6.0.7 or 6.1 source on 27 Aug 2026. The
highlights, by file:
- A02 — the env-string parsing trap (
DEBUG = os.getenv("DEBUG", "False")
is true;security.W018catches it andsecurity.W020does not catch
[""]), a new section on wildcard entries inALLOWED_HOSTSand
CSRF_TRUSTED_ORIGINS, the COOP relaxation scoped to the popup response,
the computed-salt audit gap,base-urias the CSP directive whose absence
undoes the nonce, DKIM selector rotation as a DNS deletion, DMARC and SPF
record counting, the five suppression kinds, anddiffsettings --default
as the drift instrument whose output is secret material. - A03 — the pin as the trust decision (
--require-hashesbinds the
artifact to the pin, nothing binds the pin to a person),[build-system] requiresandpip install .as the two inputs outside the hashed file,
--only-binary :all:,PIP_EXTRA_INDEX_URLread from the environment, the
attest job split so no third-party code runs besideid-token: write, and
the migration backfill routed through_base_managerso a
use_in_migrationsmanager cannot hide rows from the count. - A04 — the Argon2 memory budget multiplied by worker concurrency, the
get_random_stringentropy arithmetic,COMPARISON_KEYrotation costing a
restart rather than a migration, the shared-DEK write budget under NIST SP
800-38D, the KMS encryption context naming the column as well as the row,
and the re-encryption pass that filters on the value it read so a
concurrent write survives. - A05 — sources that cross the process boundary without a request, the
first-party wrapper searched as a sink name, template names as an input
position, interpreter arguments (-c,-e, a command after a host name)
that an argument list does not close,env=as a second argument channel,
format_htmlpassing an already-SafeStringargument through unescaped,
theDomainNameValidatorexposure shape, and the CSV renderer in
DEFAULT_RENDERER_CLASSESturning every endpoint into an export location. - A06 — ceilings for public flows with no principal, the
LimitOffsetPaginationoffset as a second multiplier, JSON list bodies
outsideDATA_UPLOAD_MAX_NUMBER_FIELDSand theListSerializer
max_lengthanswer, writers outside the repository that only a constraint
reaches, repricing onperform_update(), the provider's operation
identifier as the only key a callback may resolve a row by, and
plus-address normalization on notification limits. - A07 — recovery resolving the same principal set as login
(get_users()against the exact column), the bcrypt 72-byte truncation
against the validators that read the whole string, a mechanism for the
SP 800-63B-4 forced-change requirement, the breach validator hardened
(https pinned at load, redirect origin checked, bounded read, one
fail-open exit), the DRF login CSRF gap (APIView.as_view()is
csrf_exemptandSessionAuthenticationenforces nothing before
authentication),ModelBackend's dummy hash on a miss and the custom
backends that drop it, the IPv6 /64 lockout key, django-axes defaults read
off 8.3.1 (AXES_COOLOFF_TIME = Noneis a permanent lock), a reset never
satisfying the second factor, and re-authentication minting a bounded
artifact rather than a session flag. - A08 — the Celery remote-control channel enumerated off 5.6.3
(shutdown,revoke,pool_restart, and the rest ride broker reach),
worker_enable_remote_control, a new section binding a webhook event to a
tenant through the stored connection mapping rather than a payload field,
and the durableRECEIVEDrow surviving a lost wake-up. - A09 — the query string recorded at the proxy tier above
LOGGING,
sensitive_post_parametersraisingTypeErroron DRF views, alerts on
the single decisive event and on silence, the audit alias under
DATABASE_ROUTERS, group and permission writes asm2m_changedevents,
what a hash chain proves and what it cannot, and the decoy register with
its expected reader set. - A10 — the truthy-coroutine policy check (an
async defgate called
withoutawaitgrants everyone), negated flag names, the sign bound before
the balance guard,select_for_update()as a silent no-op on SQLite, the
anchor-row lock for aggregate invariants, the two ways a declared
constraint is absent (backend support and NULL columns),IntegrityError
caught at the write and mapped to 409,UniqueValidatoras a message
rather than enforcement,on_commitin autocommit androbust=True,
idempotency-key validation, read-path expiry, and the stored response as a
retained copy; the regex subject measured where the match happens, and
__regexlookups as a database-side engine no Python cap reaches. - DRF — update authorizing the record as stored while
serializer.save()
applies the body after, overriddendispatch()/initial()removing every
control, every@actionkeyword argument replacing the viewset's list, the
GET-default action, scopes that resolve to nothing failing open
(filter(tenant=None)isIS NULL),BasicAuthenticationin the default
list, cookie-carried tokens needing their own CSRF,ObtainAuthToken
opting out of project defaults,rest_framework.urlsmounting plain Django
login views, version switch-off by route removal, andbulk_create()on
the create path. - Authorization architecture — a permission class implementing only
has_object_permissionas the worse half of the incomplete-class defect,
autocomplete lookups scoped on the relatedModelAdminwhile
limit_choices_tobinds the write, the URLconf audit test reading
initkwargsso a per-actionAllowAnycannot hide, identity-selecting
fields never writable, the allow-list governing every serializer a route
can build, scoping applied before the aggregate stage so facet counts do
not leak, and offboarding targets marked done only from a read-back. - Data layer — sequence grants split from
SELECT, the migration
bookkeeping table revoked from the runtime role,BYPASSRLSread from
pg_roles,SECURITY DEFINERfunctions pinningsearch_path, policies on
partitions, views, and materialized views, tenant context on second
aliases and afterCOMMITpops it, the isolation level proven withSHOW transaction_isolation, retry jitter and a driver-tested classifier, and
role-level ceilings (CONNECTION LIMIT,idle_in_transaction_session_timeout). - Data lifecycle — the tombstone releasing its identifier the moment a
partial constraint frees it, delete receivers connected in
AppConfig.ready()so a worker cannot take the fast-delete path, the
credentials that survive an erasure, crypto-shredding decided at the first
write, the erasure ledger replayed from a store no restore rewrites, the
three purge-predicate defects, and masked extracts as pseudonymized data. - Deployment — the CDN-fronted origin reachable directly, the
proxy_set_headerinheritance trap withX-Forwarded-Host,X-Real-IP,
Forwarded, andClient-Certoverwritten, the client-IP function checking
addresses as well as depth and failing closed off the probe path, the
world-connectable Gunicorn socket (--umask 007),--proxy-protocol
acceptance, uvicorn and Daphne forwarded-header behavior, systemd
environments published over D-Bus (LoadCredential=instead), namedCOPY
paths, broker URL schemes, and the unauthenticated Redis rating that does
not rest on a CVE. - Uploads — bidirectional-override filenames, the promotion copy carrying
the server's verdict headers, a privateSTORAGESalias isolating nothing
until itsOPTIONSdiffer, the quarantine prefix unreadable outside the
verification tier,PENDINGcaps as the only object-count bound, SHA-256
over the bytes rather than the ETag, and the${filename}key that
downgrades boto3's exact-key condition to a prefix match. - GraphQL and non-DRF — the cost rule hardened (negative page arguments
clamped, variables and absent arguments assumed at the ceiling, fragment
spreads followed and cycles stopped), error masks keyed on
original_error, list inputs capped before the loop, the persisted-query
registry that is loaded and never written, Ninjaresponse=as the field
allow-list and its CSRF model, and gRPC metadata read as a multimap so the
authorized credential and the recorded one cannot differ. - Service identity — sender-constrained binding as verification step
eight, the module-levelPyJWKClientwith an explicit timeout,httpJWKS
URIs accepted by the library, the full DPoP proof checks and the
fleet-widejtistore, the proxy chain where the verifying hop and
REMOTE_ADDRmust be the same hop, CI federation bound to a subject, and
introspection as the only thing that shortens a leaked token's life. - Privileged access — the invariant split per mechanism (impersonation
stays below the operator, elevation is bounded by scope and time), the
credential surface denied to an impersonated session on read and write,
the server-side episode record as the authority, the audit receiver that
must raise past 500 to roll the acquire back,hijack_history[0]naming
the person in a chained acquire, release-rather-than-reject middleware,
and the target notified out of band after the episode. - Library index — the two-clocks dating rule, floors that bind the
project rather than the integration, SimpleJWT denylisting inert without
the blacklist app, thedjango-auth-ldaprow with thepython-ldap>=3.4.5
floor, guardian's anonymous-user row as a live grant target,
django-tenantspinned exactly,django-celery-beat'sPeriodicTaskas a
task-execution control plane, and the DRF 3.17.2/3.18.0 security-against-
feature split.
The language restoration
The closure added 74 prose sentences over the 25-word ceiling the v1.50.0
language pass set. Each one is split back under it, with the meaning
unchanged — no control, threshold, identifier, or date moved. The verbatim
SP 800-63B-4 blocklist requirement in the authentication file stays the one
standing exception, kept intact because a split would falsify a quoted
normative SHALL. The methodology file also drops a stale count: its ASVS
renumbering note said twenty-three files where the corpus holds twenty-five,
and the claim is now count-free.
The router rebuild
SKILL.md is rewritten to the corpus's own ASD-STE100 standard. The router
rows become keyword lists — the same trigger vocabulary, without the run-on
sentences the language pass never reached — and the file falls from 39,065 to
32,436 of the 40,960 bytes the docs-integrity workflow allows, restoring the
headroom the contribution guide names as a standing constraint. The ownership
table keeps every row and the three prose splits (path traversal,
configuration against runtime, human against machine identity). The
twenty-two-references count becomes a count-free statement, so the next
reference added cannot make it stale. Mode selection, the scripts contract,
the proof section, the stop conditions, the severity summary, and the
freshness statement all stand, tightened to the sentence rules.
AGENTS.md, GEMINI.md, and the .cursor rule become the thin pointers
they always claimed to be. Each one now states the load order, the two modes,
and the script contract, and defers the per-file routing to the SKILL.md
router — so the router has one home, and the pointers cannot drift from it.
The four issue templates and the README changelog carry the new version.
Verified on the finished tree
docs-integrity passes with 25 reference files, no orphan, balanced fences,
and SKILL.md at 32,436 of 40,960 bytes. validate-skill passes with the
description byte-identical at 1013 characters. The dangerous_patterns.py
self-test passes 49 fixtures with 29 of 29 rules covered, all three scanners
exit 0 with a kind: "summary" record on a missing path, and they report
zero findings against this repository. Every file-and-heading cross-reference
in the corpus was re-verified — 366 pairs resolve, four two-heading
continuations included. The library-index date does not move: no PyPI sweep
ran this release.