SEC2-046: A sync connection's lease is released unconditionally, so a run that outlived it frees the next run's lease (as fixed in #576) #988
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#988
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: Informational · Category: Business Logic · Reference: CWE-367 (Time-of-check Time-of-use Race Condition); OWASP A04:2021 (Insecure Design)
Location:
src/postulo/plugins/syncing.py:43(LEASE_SECONDS = 15 * 60)src/postulo/plugins/syncing.py:97-107src/postulo/plugins/views.py:580(Sync now callssync_connectiondirectly)src/postulo/core/scheduler.py:102-108(the fixed pattern, for comparison)Issue
The lease is a cache key that expires after fifteen minutes, and the
finallydeletes the key whatever it holds. A sync that takes longer than that (a large DAV address book over a slow server, the case #221 describes) loses its lease while still running; a second run takes a fresh lease and starts; when the first finishes it deletes the second's lease, and a third run can start beside the second.core/scheduler.py:102-108fixed this for the pass lease by storing a token and deleting only a key that still holds it (#576); the connection lease was not brought along. The scheduler's path is additionally guarded byclaim_connection(a conditionalUPDATEonsynced_at,syncing.py:142-157), but Sync now callssync_connectionwithout claiming, so between a person's Sync now and the scheduler the lease is the only thing that serialises the two.Impact
Two syncs of one connection run at once, which #586 describes as the DAV plugin duplicating contacts. It needs a sync longer than the lease, so it is a correctness and data-duplication problem rather than an exposure.
Fix
Mirror
scheduler.only_one_pass:The existing test at
tests/test_syncs.py:266keeps passing. Routing Sync now throughclaim_connectionas well is optional and adds little, sinceclaim_connectionguards scheduler against scheduler by thesynced_atstamp. Done when: a test intests/test_syncs.pyholds the lease under another token and assertssync_connectionleaves it in place after finishing.Related
#586 (closed) - "nothing stops one connection running twice at once": its fix added the lease and
claim_connection, and the unconditional delete is the residue. #576 (closed) - the same weakness in the scheduler's pass lease, fixed with a token compare.