SEC2-037: The JSON log scrubs only invitation and recovery tokens, so a password-reset or email-confirmation key can reach postulo.log #979
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#979
Loading…
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?
Found on 2026-10-16 in a read-only security review of
postuloat56fc9d531, checked by a second, independent pass.Severity: Low · Category: Data Exposure · Reference: CWE-532 (Insertion of Sensitive Information into Log File); OWASP A09:2021 (Security Logging and Monitoring Failures)
Location:
src/postulo/core/logs.py:155-160(SECRET_ROUTES: two routes)src/postulo/core/logs.py:189-215(scruband_plain, applied to every record's message andrequestextra)src/postulo/core/logredact.py:19-24(the access-log list: recover, invitation, confirm-email, password reset key, login code confirm)Issue
#754 settled which path segments are one-time secrets, and
logredact.redactstrips them from gunicorn's access log. Postulo's own JSON log, fixed for #733, has its own shorter list:Django's
django.requestlogger writes "Internal Server Error: %s" / "Not Found: %s" withrequest.pathand attaches the request, and_plainwritesscrub(value.path). A 500 while servingGET /accounts/password/reset/key/<uidb36>-<key>/(the one request in allauth's flow where the key is in the path) orGET /accounts/confirm-email/<key>/therefore writes the live key intopostulo.log, whichLogsViewshows every administrator and/logshands to a collector, while the same line in the access log is redacted. The reset key stays valid until the password changes orPASSWORD_RESET_TIMEOUTpasses. The confirmation key is the weaker of the two (it only marks an address verified, though #731 and #891 show why that still matters);login/code/confirm/carries no secret in its path, the code being posted in the form.Impact
Narrow: it needs a server error on the one request that carries the key, and then the reader is an administrator or whoever holds
POSTULO_LOGS_TOKEN. The consequence when it happens is the account-takeover link #754 describes, in the one log the project promises is safe to hand on.Fix
Use one list: make
logs.scrub()calllogredact.redact()after its own route-derived pattern, so the two logs redact the same segments and any future addition lands in one place. Done when:tests/test_hardening_754.pyalso formats adjango.requestrecord whoserequest.pathis a reset-key and a confirm-email path throughJSONFormatterand asserts the key is gone from both the message and therequestfield.Related
#733 (closed) - fixed the query string and two path tokens in the JSON log. #754 (closed) - fixed the access log with the fuller list. The gap between the two lists is new.