
Educational PoC et analyse de CVE-2025-68621, une attaque temporelle sur la connexion de synchronisation de Trilium Notes. Démontre la récupération du hachage HMAC via un canal auxiliaire temporel réseau, avec les détails de la cause racine, du correctif et de l'atténuation.
/api/login/syncSévérité : HAUTE (CVSS 7.4) Logiciel affecté : TriliumNext/Trilium < 0.101.0 Type de vulnérabilité : CWE-208 – Discrepance temporelle observable Corrigé dans : Trilium 0.101.0 (PR #8129) Publié : 2026-02-06 | Réservé : 2025-12-19
Trilium Notes est une application open-source, multiplateforme, de prise de notes hiérarchique conçue pour construire de grandes bases de connaissances personnelles. Elle prend en charge :
La fonctionnalité de synchronisation permet à un client Trilium de s'authentifier auprès d'un serveur Trilium afin que les notes restent synchronisées entre les appareils. Ce point de terminaison de synchronisation est la porte d'entrée de CVE-2025-68621.
Une attaque temporelle est une attaque par canal auxiliaire où un attaquant apprend une information secrète en mesurant combien de temps un système met à traiter différentes entrées.
L'exemple classique est la comparaison de chaînes :
"correct_password" !== "aorrect_password" → échec à la position 0 → rapide
"correct_password" !== "cXrrect_password" → échec à la position 1 → légèrement plus lent
"correct_password" !== "correct_password" → correspondance complète → le plus lent
La plupart des langages de programmation comparent les chaînes caractère par caractère et s'arrêtent dès qu'une différence est trouvée (sortie anticipée). Cela signifie :
La correction consiste à utiliser une fonction de comparaison en temps constant qui examine toujours chaque octet indépendamment de l'endroit où une différence se produit.
La vulnérabilité a été découverte par révision manuelle du code de la logique d'authentification de Trilium. Le chercheur a examiné le flux de connexion de synchronisation dans apps/server/src/routes/api/login.ts et a remarqué le motif suivant dans la fonction loginSync() (autour de la ligne 111) :
const documentSecret = options.getOption("documentSecret");
const expectedHash = utils.hmac(documentSecret, timestampStr);
const givenHash = req.body.hash;
if (expectedHash !== givenHash) { // ← LIGNE VULNÉRABLE
return [400, { message: "Sync login credentials are incorrect..." }];
}
Le signal d'alarme est l'utilisation de l'opérateur !== natif de JavaScript pour comparer des hachages HMAC. L'opérateur !== n'est pas en temps constant — il sort dès qu'il trouve un caractère différent. Comme la comparaison est effectuée sur des chaînes simples (sans utiliser de fonction de comparaison cryptographiquement sûre), le temps de réponse divulgue des informations sur le nombre d'octets de tête de la supposition de l'attaquant qui sont corrects.
Le chercheur s'est alors demandé :
"Cette petite différence temporelle peut-elle être suffisamment amplifiée, à travers un réseau, pour récupérer le hachage HMAC complet encodé en Base64 de 44 caractères ?"
La réponse s'est avérée être oui — avec suffisamment de mesures répétées et une analyse statistique, le signal dépasse le bruit.
Lorsqu'un client Trilium souhaite se synchroniser, il appelle POST /api/login/sync avec un corps JSON comme :
{
"timestamp": "2025-12-19T10:00:00.000Z",
"syncVersion": 34,
"hash": "<HMAC-SHA256 of documentSecret + timestamp, Base64-encoded>"
}
La récupération octet par octet fonctionne comme suit :
Pour position = 0 jusqu'à 43 :
Pour chaque caractère candidat c dans le jeu de caractères (A-Z, a-z, 0-9, +, /, =) :
Envoyer ÉCHANTILLONS requêtes avec hash = préfixe_connu + c + bourrage
Enregistrer le temps de réponse moyen
Meilleur caractère = candidat avec le temps moyen le plus élevé
Ajouter le meilleur caractère au préfixe_connu
Après 44 itérations (une par caractère Base64), le hachage HMAC complet de 44 caractères est récupéré.
Exigences pratiques :
time.perf_counter() en Python donne une résolution nanoseconde)Voir poc.py pour un PoC Python entièrement annoté.
Résumé rapide de ce que fait le PoC :
A–Z, a–z, 0–9, +, /, =)./api/login/sync pour chaque candidat et mesure le temps de réponse médian.Avertissement : Ce PoC est fourni à des fins éducatives et de recherche en sécurité responsable uniquement. Ne l'utilisez pas contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation écrite explicite de tester.
Les opérateurs !== (et ===) de JavaScript effectuent une comparaison lexicographique avec sortie anticipée. La ligne vulnérable dans apps/server/src/routes/api/login.ts :
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
Le comportement de sortie anticipée crée une différence temporelle mesurable par octet correspondant :
Chaque octet supplémentaire correspondant coûte une minuscule quantité de temps CPU δ. Sur des milliers d'échantillons, le temps de réponse moyen pour une supposition "octet N correct" est mesurablement plus long que pour une supposition "octet N incorrect", divulguant suffisamment d'informations pour récupérer le hachage HMAC complet caractère par caractère.
Chaîne vectorielle : CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Une exploitation réussie donne à l'attaquant :
C'est particulièrement grave pour les utilisateurs qui stockent des données personnelles sensibles (mots de passe, documents privés, entrées de journal) dans leur base de connaissances Trilium.
La correction remplace la comparaison non en temps constant !== par la fonction intégrée crypto.timingSafeEqual() de Node.js :
Avant (vulnérable) :
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
Après (sécurisé) :
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() compare toujours chaque octet, donc le temps d'exécution ne dépend pas du nombre d'octets correspondants. Le signal temporel disparaît.
Voir vulnerable.ts et fix.ts pour des exemples de code côte à côte.
Si vous exécutez un serveur Trilium auto-hébergé, mettez à jour immédiatement vers la version 0.101.0 ou ultérieure.
# Exemple Docker
docker pull zadam/trilium:0.101.0
N'utilisez jamais === / !== pour comparer des secrets. Les opérateurs d'égalité de JavaScript ne sont pas en temps constant. Toute comparaison de HMAC, jetons ou mots de passe utilisant === / !== est un oracle temporel potentiel.
Utilisez toujours crypto.timingSafeEqual() dans Node.js (ou un équivalent dans votre langage/environnement d'exécution) lors de la comparaison de valeurs cryptographiques. C'est l'API standard et spécialement conçue pour cette tâche.
Les attaques temporelles sont réelles sur le réseau. Bien que des différences nanosecondes semblent impossibles à détecter sur Internet, les techniques statistiques et un nombre suffisant d'échantillons peuvent extraire un signal clair des mesures bruitées — en particulier dans des environnements à faible gigue.
La limitation de débit seule n'est pas une mitigation suffisante. Même avec une limitation de débit par IP, un attaquant ayant accès à des proxies rotatifs ou un botnet peut toujours accumuler suffisamment d'échantillons pour exploiter la différence temporelle.
La vérification HMAC mérite la même attention que la comparaison de mots de passe. Les hachages HMAC sont des secrets. Traitez toute comparaison de valeur secrète comme si les canaux auxiliaires temporels pouvaient être exploités.
La revue de code pour les motifs cryptographiques est essentielle. Cette vulnérabilité a été trouvée par une revue manuelle — une seule ligne de code qui semblait inoffensive mais qui avait de sérieuses implications de sécurité. Des audits dédiés crypto/sécurité aident à détecter ces problèmes tôt.
Ce dépôt est maintenu à des fins éducatives et de recherche dans le cadre des principes de divulgation responsable.
| Saisie vs. Attendu | Octets comparés | Temps |
|---|
| Mauvais octet 0 | 1 | ~T |
| Octet 0 correct, mauvais octet 1 | 2 | ~T + δ |
| Octets 0–1 corrects, mauvais octet 2 | 3 | ~T + 2δ |
| … | … | … |
| Les 44 octets corrects | 44 | ~T + 43δ |
| Métrique | Valeur | Raison |
|---|
| Score de base | 7.4 HAUTE | |
| Vecteur d'attaque | Réseau (N) | Exploitable via Internet |
| Complexité d'attaque | Élevée (H) | Nécessite de nombreuses requêtes + une temporisation stable |
| Privilèges requis | Aucun (N) | Aucun compte nécessaire |
| Interaction de l'utilisateur | Aucune (N) | La victime n'a rien à faire |
| Périmètre | Inchangé (U) | Seul le serveur Trilium est affecté |
| Confidentialité | Élevée (H) | L'intégralité de la base de notes est lisible |
| Intégrité | Élevée (H) | L'attaquant peut écrire/modifier des notes |
| Disponibilité | Aucune (N) | Aucun composant de déni de service |
| Date | Événement |
|---|
| 2025-12-19 | CVE-2025-68621 réservé par GitHub Security |
| 2025-12-21 | PR de correction #8129 ouverte |
| 2025-12-25 | PR fusionnée ; Trilium 0.101.0 publié |
| 2026-02-06 | CVE publié publiquement |
| 2026-02-09 | Enrichissement ADP de la CISA ajouté |
| Ressource | Lien |
|---|
| Avis de sécurité GitHub | GHSA-hxf6-58cx-qq3x |
| Pull Request de correction | TriliumNext/Trilium#8129 |
| Enregistrement CVE (CVEProject) | CVE-2025-68621.json |
| CWE-208 | Discrepance temporelle observable |
| Dépôt Trilium Notes | TriliumNext/Trilium |