
PoC educativo y análisis de CVE-2025-68621, un ataque de sincronización en el inicio de sesión de sincronización de Trilium Notes. Demuestra la recuperación de hash HMAC mediante un canal lateral de sincronización de red, con causa raíz, corrección y detalles de mitigación.
/api/login/syncGravedad: ALTA (CVSS 7.4) Software afectado: TriliumNext/Trilium < 0.101.0 Tipo de vulnerabilidad: CWE-208 – Discrepancia de temporización observable Corregido en: Trilium 0.101.0 (PR #8129) Publicado: 2026-02-06 | Reservado: 2025-12-19
Trilium Notes es una aplicación de toma de notas jerárquica, de código abierto y multiplataforma, diseñada para construir grandes bases de conocimiento personales. Admite:
La función de sincronización permite que un cliente Trilium se autentique en un servidor Trilium para que las notas permanezcan sincronizadas entre dispositivos. Este endpoint de sincronización es el punto de entrada de CVE-2025-68621.
Un ataque de temporización es un ataque de canal lateral en el que un atacante obtiene información secreta midiendo cuánto tiempo tarda un sistema en procesar diferentes entradas.
El ejemplo clásico es la comparación de cadenas:
"correct_password" !== "aorrect_password" → fails at position 0 → fast
"correct_password" !== "cXrrect_password" → fails at position 1 → slightly slower
"correct_password" !== "correct_password" → matches fully → slowest
La mayoría de los lenguajes de programación comparan cadenas carácter por carácter y se detienen en cuanto encuentran una discrepancia (salida anticipada). Esto significa:
La solución es utilizar una función de comparación en tiempo constante que siempre inspeccione todos los bytes, independientemente de dónde se produzca una discrepancia.
La vulnerabilidad se descubrió mediante revisión manual de código de la lógica de autenticación de Trilium. El investigador examinó el flujo de inicio de sesión de sincronización en apps/server/src/routes/api/login.ts y observó el siguiente patrón en la función loginSync() (alrededor de la línea 111):
const documentSecret = options.getOption("documentSecret");
const expectedHash = utils.hmac(documentSecret, timestampStr);
const givenHash = req.body.hash;
if (expectedHash !== givenHash) { // ← VULNERABLE LINE
return [400, { message: "Sync login credentials are incorrect..." }];
}
La señal de alerta es el uso del operador !== incorporado de JavaScript para comparar hashes HMAC. El operador !== no es de tiempo constante: sale en cuanto encuentra un carácter diferente. Dado que la comparación se realiza sobre cadenas de texto simples (sin utilizar una función de comparación criptográficamente segura), el tiempo de respuesta filtra información sobre cuántos bytes iniciales de la suposición del atacante son correctos.
El investigador se preguntó entonces:
"¿Puede esta pequeña diferencia de temporización amplificarse lo suficiente, a través de una red, como para recuperar el hash HMAC completo de 44 caracteres codificado en Base64?"
La respuesta resultó ser sí — con suficientes mediciones repetidas y algo de análisis estadístico, la señal supera al ruido.
Cuando un cliente Trilium quiere sincronizar, llama a POST /api/login/sync con un cuerpo JSON como:
{
"timestamp": "2025-12-19T10:00:00.000Z",
"syncVersion": 34,
"hash": "<HMAC-SHA256 of documentSecret + timestamp, Base64-encoded>"
}
La recuperación byte a byte funciona de la siguiente manera:
For position = 0 to 43:
For each candidate character c in charset (A-Z, a-z, 0-9, +, /, =):
Send SAMPLES requests with hash = known_prefix + c + padding
Record average response time
Best character = candidate with highest average time
Append best character to known_prefix
Tras 44 iteraciones (una por carácter Base64), se recupera el hash HMAC completo de 44 caracteres.
Requisitos prácticos:
time.perf_counter() en Python ofrece resolución de nanosegundos)Consulte poc.py para ver un PoC de Python totalmente comentado.
Resumen rápido de lo que hace el PoC:
A–Z, a–z, 0–9, +, /, =)./api/login/sync para cada candidato y mide el tiempo de respuesta medio.Aviso legal: Este PoC se proporciona únicamente con fines educativos y de investigación de seguridad responsable. No lo utilice contra sistemas que no le pertenezcan o para los que no tenga permiso explícito por escrito.
Los operadores !== (y ===) de JavaScript realizan una comparación lexicográfica con salida anticipada. La línea vulnerable en apps/server/src/routes/api/login.ts:
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
El comportamiento de salida anticipada crea una diferencia de temporización medible por cada byte coincidente:
| Suposición vs. esperado | Bytes comparados | Tiempo |
|---|---|---|
| Byte 0 incorrecto | 1 | ~T |
| Byte 0 correcto, byte 1 incorrecto | 2 | ~T + δ |
| Bytes 0–1 correctos, byte 2 incorrecto | 3 | ~T + 2δ |
| … | … | … |
| Los 44 bytes correctos | 44 | ~T + 43δ |
Cada byte adicional coincidente cuesta una pequeña cantidad extra de tiempo de CPU δ. A lo largo de miles de muestras, el tiempo de respuesta medio para una suposición con el "byte N correcto" es mediblemente mayor que para una con el "byte N incorrecto", filtrando suficiente información para recuperar el hash HMAC completo carácter por carácter.
| Métrica | Valor | Razón |
|---|---|---|
| Puntuación base | 7.4 ALTA | |
| Vector de ataque | Red (N) | Explotable a través de internet |
| Complejidad de ataque | Alta (H) | Requiere muchas solicitudes + temporización estable |
| Privilegios requeridos | Ninguno (N) | No se necesita cuenta |
| Interacción del usuario | Ninguna (N) | La víctima no necesita hacer nada |
| Alcance | Sin cambios (U) | Solo se ve afectado el servidor Trilium |
| Confidencialidad | Alta (H) | Toda la base de notas es legible |
| Integridad | Alta (H) | El atacante puede escribir/modificar notas |
| Disponibilidad | Ninguna (N) | Sin componente de denegación de servicio |
Cadena de vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Una explotación exitosa otorga al atacante:
Esto es especialmente grave para los usuarios que almacenan datos personales sensibles (contraseñas, documentos privados, entradas de diario) en su base de conocimiento de Trilium.
La solución sustituye la comparación !== que no es de tiempo constante por la función integrada crypto.timingSafeEqual() de Node.js:
Antes (vulnerable):
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
Después (seguro):
import * as crypto from "crypto";
const expectedBuffer = Buffer.from(expectedHash);
const givenBuffer = Buffer.from(givenHash ?? "");
if (expectedBuffer.length !== givenBuffer.length ||
!crypto.timingSafeEqual(expectedBuffer, givenBuffer)) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
crypto.timingSafeEqual() siempre compara todos los bytes, por lo que el tiempo de ejecución no depende de cuántos bytes coincidan. La señal de temporización desaparece.
Consulte vulnerable.ts y fix.ts para ver ejemplos de código lado a lado.
Si está ejecutando un servidor Trilium autoalojado, actualice a la versión 0.101.0 o posterior inmediatamente.
# Docker example
docker pull zadam/trilium:0.101.0
Nunca utilice === / !== para comparar secretos. Los operadores de igualdad de JavaScript no son de tiempo constante. Cualquier comparación de HMAC, tokens o contraseñas que utilice === / !== es un posible oráculo de temporización.
Utilice siempre crypto.timingSafeEqual() en Node.js (o un equivalente en su lenguaje/entorno de ejecución) al comparar valores criptográficos. Es la API estándar y específica para esta tarea.
Los ataques de temporización son reales a través de la red. Aunque las diferencias de nanosegundos parezcan imposibles de detectar en internet, las técnicas estadísticas y suficientes muestras pueden extraer una señal clara de mediciones ruidosas — especialmente en entornos de baja fluctuación.
La limitación de frecuencia por sí sola no es una mitigación suficiente. Incluso con limitación por IP, un atacante con acceso a proxies rotativos o una botnet puede seguir acumulando suficientes muestras para explotar la diferencia de temporización.
La verificación HMAC merece el mismo cuidado que la comparación de contraseñas. Los hashes HMAC son secretos. Trate cualquier comparación de un valor secreto como si los canales laterales de temporización pudieran ser explotados.
La revisión de código en busca de patrones criptográficos es esencial. Esta vulnerabilidad se encontró mediante revisión manual: una sola línea de código que parecía inofensiva pero tenía serias implicaciones de seguridad. Las auditorías dedicadas de criptografía/seguridad ayudan a detectar estos problemas a tiempo.
| Fecha | Evento |
|---|---|
| 2025-12-19 | CVE-2025-68621 reservado por GitHub Security |
| 2025-12-21 | Abierto el PR de corrección #8129 |
| 2025-12-25 | PR fusionado; lanzada Trilium 0.101.0 |
| 2026-02-06 | CVE publicado públicamente |
| 2026-02-09 | Enriquecimiento ADP de CISA añadido |
| Recurso | Enlace |
|---|---|
| Aviso de seguridad de GitHub | GHSA-hxf6-58cx-qq3x |
| Pull request de corrección | TriliumNext/Trilium#8129 |
| Registro CVE (CVEProject) | CVE-2025-68621.json |
| CWE-208 | Discrepancia de temporización observable |
| Repositorio de Trilium Notes | TriliumNext/Trilium |
Este repositorio se mantiene con fines educativos y de investigación bajo principios de divulgación responsable.