SEC2-006: Check-then-create without a lock on a grant and on a group name #948
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#948
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-362 (Race Condition); OWASP A04:2021 (Insecure Design)
Location:
src/postulo/jobs/sharing.py:164-174(_grant)src/postulo/core/views_access.py:60-71(GroupsView.post,create, and thegrouppk parse)Issue
Two concurrent shares of the same record to the same grantee (two tabs, or a double submit) both see
Noneand the second hitsone_share_per_<record>_per_<grantee>; inside@transaction.atomicthat is anIntegrityErrorand a 500. LikewiseGroup.objects.filter(name__iexact=name).exists()thenGroup.objects.create(name=name)races into the uniquenamecolumn. Finallyget_object_or_404(Group, pk=request.POST.get("group", "0") or 0)is handed the raw POST value; a non-numeric string raisesValueErrorfrom the field, which is a 500 rather than a 404 (staff only). No information is disclosed by the error page; the answer is simply the wrong status.Impact
Staff-only or same-account effects; no cross-account consequence. Recorded for completeness beside #544 and #738, which fixed the same pattern where it mattered.
Fix
Use
Share.objects.get_or_create(**lookup, defaults={"scope": scope, "granted_by": actor})in_grantand widen the scope afterwards, which keeps the widening rule intact. WrapGroup.objects.createintry/except IntegrityErrorinside its owntransaction.atomic()savepoint: on PostgreSQL anIntegrityErrorinside the outer atomic block poisons the transaction otherwise. Parse the pk withint()in atry, orpk.isdigit()as_addalready does. Done when a test creates the same share twice and gets one row with the wider scope, and a test posts a non-numericgroupvalue and gets a 404.Related
#544 (closed) and #738 (closed) - the same pattern on invitations and first-account creation; these two spots are new with #896/#886. #767 (closed) - the same check on other endpoints.