CodeQL: a small cluster of real quality findings, and the guards it cannot see #249
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Postulo/postulo#249
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Run on 2026-09-16 with the CodeQL CLI 2.27.0 (the toolchain the VS Code extension ships), pack
codeql/python-queries@1.8.10, suitepython-security-and-quality, overmainat23c742c7f. 131 results, 354 source files.No security vulnerability survived review. All 12 error-severity and all 4
warning-severity findings are false positives. The reasons are recorded here so the next run
is not triaged from scratch:
py/full-ssrf(9.1)jobs/logos.py:112http.public_only_client, which checks every hop and connects to the address it checked (#215). CodeQL does not model that client.py/url-redirection(6.1)core/arranging.py:121mode_url()always starts fromreverse("core:home"), andkeyis refused unless it is inwidgets.REGISTRY.py/log-injection(6.1) x3plugins/registry.py:204,211,219%r, andrepr()escapes newlines.py/log-injection(6.1)core/server_views.py:957^[a-z0-9][a-z0-9._-]{1,30}[a-z0-9]$, which admits no newline.py/stack-trace-exposure(5.4) x2core/views_logs.py:93,core/views_metrics.py:54str(throttle.TooOften)— a "try again in N seconds" line, not a trace. Both endpoints are token-guarded.py/incomplete-url-substring-sanitization(7.8)tests/test_image_build.py:181runtimeis Dockerfile text, not a URL.py/catch-base-exceptionplugins/registry.py:197sys.exitat import time must not end the worker (#228), andKeyboardInterruptis re-raised just above.py/ineffectual-statementx18plugins/base.py,documents/pdf.py,core/channels.py,notifications/base.py...inProtocolmethod stubs.py/unreachable-statement,py/uninitialized-local-variabletests/test_document_record.py:188,tests/test_documents.py:380pytest.raisesnorpytest.skip.What is worth doing
All small, and none of it urgent.
tests/test_mail_destinations.py:299and
tests/test_mail_xoauth2.py:208readassert backend.open() is Falseandassert backend.open() is True. Underpython -Othe assert is stripped and the callgoes with it, so the test would pass without ever opening anything. Bind first, then
assert the name.
resume/views.py:187buildsf"...?language={language}", andtranslating.normalise()only strips, lowercases andswaps
_for-— it never checks the code against a known language. The host is fixedby
reverse(), so this is not an open redirect and is not a security finding; it is areflected value that should go through
urlencodebefore it reaches a query string.__init__.jobs/forms.py:34:OwnerScopedModelForm.__init__callsself.scope_querysets(), whichApplicationForm,ReminderFormand two others override. The override runs while the subclass's own__init__is still unfinished. It works today only because the one thing it needs,self.user, is assigned beforesuper().__init__()— which is a fact about the currentsubclasses, not a promise the base class makes.
tests/test_email_settings.py:362assignsresponsefroma
client.postand never reads it; the assertion below checksSiteSettingsinstead.Either an assertion about the response is missing or the binding should go.
except: pass.core/csv_import.py:484(falls through to a delimiterheuristic),
accounts/deletion.py:134and:139(a directory that will not remove).All three are deliberate and all three want one line saying so.
core/logs.py:235isflagged by the same rule but explains itself in its docstring already.
ids=lambda value: str(value)isids=str(
tests/test_slow_requests.py:62,67); a module imported twice intests/test_brand.py:70,tests/test_navigation.py:62andtests/test_phones.py:220;import xbesidefrom x import yinaccounts/migrations/0003_username_and_verified_addresses.py:12andtests/security/test_admin_exposure.py:36; and the adjacent byte literals atseed_demo.py:234build one PDF object deliberately but read exactly like a missing comma.from .settings import *inconfig/settings/{dev,prod,test}.pyis flagged aspy/polluting-import. That is how Django settings are layered — leave it.The 79 import cycles are their own matter: #248.
python-security-experimentalas well, 2026-09-16 — nothing new to fixSame database, same commit (
23c742c7f), suitepython-security-experimental(79 queries,27 of them not in
python-security-and-quality). 75 results, none of them real, so noissue was opened for it. Recorded here so the next run is not triaged from scratch.
The 27 new queries all ran and all returned nothing. Confirmed by their presence in the
SARIF rule table with zero results, not by their absence:
py/zipslip,py/tarslip,py/tarslip-extended,py/unsafe-unpacking,py/csv-injection,py/decompression-bomb,py/unicode-dos,py/unicode-bypass-validation,py/insecure-randomness,py/jwt-empty-secret-or-algorithm,py/jwt-missing-verification,py/cors-misconfiguration-with-credentials,py/prompt-injection,py/xslt-injection,py/js2py-rce,py/improper-ldap-auth,py/insecure-ldap-auth,py/ldap-injection.Two of those deserve a note:
(#228, #246) and backup restore (#234, #242) both take an archive from an operator. They
are clean, and the code says why:
backup.py::_safe_member_pathrefuses any member thatis absolute or contains
.., refuses anything that is not a plain file, and checks everymember before writing any of them.
installing.py::read_wheelandimporter.py::_extractonly ever
read()into memory — neither extracts to disk.py/flask-constant-secret-keyis Flask-only and cannot see a DjangoSECRET_KEY, so itssilence is not evidence about #234's key handling.
The other 65 results are two timing-attack queries, and all 65 are false positives.
tests/, comparing values inside assertions.backup.py:453andcatalogue.py:313verify anarchive against a digest the same party supplied, and
idempotency.py:90compares requestfingerprints. A timing leak tells an attacker something they already hold.
plugins/forms.py:152,server_forms.py:301,provenance.py:161andkeys.py:83areflagged for
inanddict.getagainst field names and a placeholder list, not secrets.server_views.py:1181(token != pending.get("token")) is the closest to real and isstill not: the token it compares against lives in the caller's own session, so the only
token anybody can time is one they already know. The equality check also has to pass before
tokenreachesf"{token}.whl", which is what keeps that path safe.The bearer-token endpoints that would have been the real finding already use
hmac.compare_digest—views_logs.py:63andviews_metrics.py:34.Conclusion for #233's nightly:
python-security-experimentalcosts a full extra run andfound nothing in two attempts. Worth repeating only when the archive, token or crypto paths
change;
python-security-and-qualityis the suite to schedule. If it is ever run in CI,py/possible-timing-attack-sensitive-infoandpy/possible-timing-attack-against-hashneedexcluding or they are 65 results of noise on their own.