SEC2-001: The share form confirms whether an email address or username has an account, and maps an address to its username, unthrottled #943
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#943
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-204 (Observable Response Discrepancy); CWE-770 (Allocation of Resources Without Limits); OWASP A01:2021 (Broken Access Control) / A07:2021 (Identification and Authentication Failures)
Location:
src/postulo/jobs/sharing.py:262-284(grantee_from)src/postulo/jobs/share_views.py:45-83(ShareView.post)Issue
Any signed-in member who owns one company, contact or listing can open its Sharing page and type a value into With a person. The lookup resolves it against every active account by username or email address:
The view answers differently for a hit and a miss, and names the account on a hit:
grantee_labelreturnsgrantee.get_username(), and the grants list on the same page printsgrant.grantee_user.get_usernamefor each share. So typing an email address tells the sharer (a) that an account with that address exists on this instance and (b) which username it belongs to. Nothing throttles the view: neithershare_views.pynorsharing.pyusespostulo.core.throttle. The template's own comment says "an instance's accounts are not a directory to pick from", but the lookup behaves as a directory for anybody who types.#745 fixed the same class of oracle on the sign-up form and Settings → Account by bounding username attempts to
POSTULO_USERNAME_RATE(10/h). The sharing feature (#896), landed after that audit, reintroduced an unbounded one that also covers email addresses, which the earlier surfaces never answered for. The refusal wording is already identical for an unknown address, a deactivated account's address and the sharer's own address (share_views.py:52); the gap is the hit, and the volume.Impact
A signed-in member (the lowest-privileged account on a shared instance) can test a list of email addresses against the instance and learn which colleagues, family members or strangers hold an account here and under which username; a recruiter's or a competitor's address can be confirmed. It also hands them the username needed for the per-account sign-in lock of #489/#744 and for targeted sharing. Each hit creates a share, which is undone only by revoking, so a probe would use read-only scope and revoke afterwards.
Fix
ShareView.post, consume a per-account throttle before resolving the grantee, asAccountForm.clean_usernamedoes, keyed onrequest.userand spent only whenvalueis non-empty and does not start withgroup:(choosing one's own group never costs allowance, asAccountFormexempts one's own name): with a newPOSTULO_SHARE_LOOKUP_RATE(default20/h, in line withPOSTULO_NUMBER_RATE) inconfig/settings/base.pybesidePOSTULO_USERNAME_RATE.tests/test_share_flow.py:56-60, which shares by address, changes with it), or keep addresses and show the typed address rather than the username on a hit and in the grants list.tests/security/test_rate_limits.pyposts N+1 distinct lookups tojobs:shareand sees the last refused, and a test asserts the refusal wording is identical for an unknown address, a deactivated account's address and the sharer's own address.Related
#745 (closed) - the same enumeration class on the sign-up form and Settings → Account, fixed there by a throttle; this is a new, unthrottled surface that also resolves email addresses and reveals the username, so an incomplete fix rather than a duplicate. #744 (closed) - the per-account sign-in lock the username feeds. #896 (closed) - the feature itself; its body does not discuss enumeration. #268 (open) - background only; its Part 3 is a design discussion and does not cover enumeration.