PST-025: The shipped gunicorn access log writes one-time bearer tokens that travel in URL paths #754
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#754
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 in the security audit of 2026-10-10 (
SECURITY-AUDIT-2026-10-10.md), run againstmainatc30851bfrom 8 to 10 October 2026, as PST-025. Severity: Low.The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.
docker/Dockerfile(370-376)Who can do it, and what they need. Anyone who can read the container's stdout or the log pipeline it feeds (docker logs, a Loki/ELK stack, a hosting dashboard) but who has no database access, for example a wider ops or support team. A reverse proxy's own access log has the same content.
How it works.
"GET /accounts/recover/<TOKEN>/ HTTP/1.1" 302.Impact. One-time account-takeover and registration credentials are persisted outside the database in a place the design says they never reach. An MFA-protected account is still protected, but one without a second factor is taken over silently.
Evidence.
Verification (verdict: confirmed; severity after verification: Low). Keep severity low. Fix the claim as follows. Drop the OAuth
code/stateitem, which is not replayable because the code is already redeemed before the line is written and the state is session-bound and deleted on use. Drop the email-confirmation key as an account-takeover item, since it only lets a reader mark an address verified. The real, replay-proven set is the recovery link (up to 1 hour, and the window starts at the first GET, which a link-preview bot or the member makes), the invitation (14 days until accepted, opens signup on a closed instance) and the allauth password-reset key (Django PASSWORD_RESET_TIMEOUT, usable until the password changes). Name the attacker as an operator of a log pipeline or hosting dashboard who has no root, docker-group or database access. Anyone withdocker logsis root-equivalent and out of scope per the threat model. Note that the operator's reverse proxy writes the same URLs, so the image's format is only one source. Impact is bounded: a second factor still blocks sign-in after the password is set, the legitimate holder loses the race visibly, and the attacker must act before the real recipient finishes. …Suggested remediation. Log a redacted request path: use a custom gunicorn logger or access-log filter that masks the path segment after /accounts/recover/, /accounts/invitation/ and /accounts/password/reset/key/ and drops query strings on the consent callback. Alternatively move tokens out of the URL path as the recovery form already does after the first request.
Fixed in
79e305281on main.postulo.log#979