Summary
sha1_activation_key() derived email-confirmation tokens from a 20-bit salt (sha1(str(random.random()))[:5]) combined with an attacker-known value (the attacker's own chosen username, or the victim's known email address).
Because the confirmation endpoint (GET /email/confirm/<key>/) was completely unauthenticated, unthrottled, and returns a response that distinguishes valid from invalid keys, an attacker could register using a donor's email address to upgrade their pre-existing "stub" account, then brute-force the resulting 2^20 keyspace online (~524,288 requests on average) to confirm the account and log in as the victim.
Our logs indicate no bulk crawling of this endpoint over the past three years, giving us high-confidence this wasn't exploited during that time.
Severity
Medium (CVSS 3.1: 6.5)
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
- Attack Vector: Network — entirely over unauthenticated HTTP requests to public endpoints.
- Attack Complexity: Low — no race condition, no MITM, no privileged vantage point required; a straightforward, deterministic online brute-force script against an endpoint with zero rate limiting.
- Privileges Required: None — both
register and confirm_email are reachable by anonymous users.
- User Interaction: None — the victim (donor) need not click anything or take any action.
- Scope: Unchanged.
- Confidentiality Impact: Low. A taken-over stub account exposes only a mailing address. It does not expose a name (
create_stub_account() has a separate bug that dropped first_name/last_name before they're ever saved), donation history (the account's donations page only links out to Neon's separately-authenticated donor portal — it renders no transaction data itself), or a pre-existing API token (a stub account, never having been logged into before, never generated one; an attacker could only mint a new one under the hijacked identity).
- Integrity Impact: Low. The attacker does gain the ability to log in and to change the account's email/password, permanently displacing the original donor from that record — but there's essentially no other legitimate data on a bare stub account for them to corrupt and such accounts could be restored by customer support.
- Availability Impact: None.
CWE
- CWE-330: Use of Insufficiently Random Values (20-bit token entropy from a non-CSPRNG source)
- CWE-307: Improper Restriction of Excessive Authentication Attempts (no rate limiting on
confirm_email)
- CWE-203: Observable Discrepancy (the
confirm.html template renders distinguishable content for invalid vs. valid/expired keys, enabling an online oracle)
Execution chain
- Attacker
POST /register/ with email=<victim_donor_email>, username=<attacker_username>, password1/2=<attacker_password> (solves hCaptcha once). The existing donor stub account is upgraded in place; activation_key = sha1(salt + attacker_username) is stored with a 5-day expiry.
- Attacker precomputes all 2^20 candidate keys offline:
sha1(f"{salt:05x}" + attacker_username) for salt in 0..0xFFFFF.
- Attacker sends
GET /email/confirm/<candidate_key>/ for each candidate, unthrottled. On average ~524,288 requests are needed before the response body is no longer the "invalid" template.
- The matching request sets
email_confirmed = True on the (now attacker-controlled) account.
- Attacker
POST /sign-in/ with their chosen username/password — succeeds because is_active=True and email_confirmed=True. The attacker now controls the account — including the mailing address it holds — and can change its email/password, permanently locking the original donor out of their own record.
Impact
- Full takeover of any donor stub account whose email address is known to the attacker (donor emails are frequently obtainable — public filings, leaked lists, direct knowledge of a specific target).
- No exposure of donation history nor donor name.
- Exposure of donor address.
- No CAPTCHA, no rate limit, and no anomaly detection stands between the attacker and success — the attack is fully automatable and requires no elevated network position.
Remediation
sha1_activation_key() is now replaced with a CSPRNG token (e.g. secrets.token_hex(20).
confirm_email is now rate-limited per IP.
activation_keys are now invalidated immediately after a successful confirmation so keys are single-use.
Credit
This vulnerability was discovered and reported by bugbunny.ai.
Summary
sha1_activation_key()derived email-confirmation tokens from a 20-bit salt (sha1(str(random.random()))[:5]) combined with an attacker-known value (the attacker's own chosen username, or the victim's known email address).Because the confirmation endpoint (
GET /email/confirm/<key>/) was completely unauthenticated, unthrottled, and returns a response that distinguishes valid from invalid keys, an attacker could register using a donor's email address to upgrade their pre-existing "stub" account, then brute-force the resulting 2^20 keyspace online (~524,288 requests on average) to confirm the account and log in as the victim.Our logs indicate no bulk crawling of this endpoint over the past three years, giving us high-confidence this wasn't exploited during that time.
Severity
Medium (CVSS 3.1: 6.5)
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:Nregisterandconfirm_emailare reachable by anonymous users.create_stub_account()has a separate bug that droppedfirst_name/last_namebefore they're ever saved), donation history (the account's donations page only links out to Neon's separately-authenticated donor portal — it renders no transaction data itself), or a pre-existing API token (a stub account, never having been logged into before, never generated one; an attacker could only mint a new one under the hijacked identity).CWE
confirm_email)confirm.htmltemplate renders distinguishable content for invalid vs. valid/expired keys, enabling an online oracle)Execution chain
POST /register/withemail=<victim_donor_email>,username=<attacker_username>,password1/2=<attacker_password>(solves hCaptcha once). The existing donor stub account is upgraded in place;activation_key = sha1(salt + attacker_username)is stored with a 5-day expiry.sha1(f"{salt:05x}" + attacker_username)forsaltin0..0xFFFFF.GET /email/confirm/<candidate_key>/for each candidate, unthrottled. On average ~524,288 requests are needed before the response body is no longer the "invalid" template.email_confirmed = Trueon the (now attacker-controlled) account.POST /sign-in/with their chosen username/password — succeeds becauseis_active=Trueandemail_confirmed=True. The attacker now controls the account — including the mailing address it holds — and can change its email/password, permanently locking the original donor out of their own record.Impact
Remediation
sha1_activation_key()is now replaced with a CSPRNG token (e.g.secrets.token_hex(20).confirm_emailis now rate-limited per IP.activation_keys are now invalidated immediately after a successful confirmation so keys are single-use.Credit
This vulnerability was discovered and reported by bugbunny.ai.