
Lehrreicher PoC und Analyse von CVE-2025-68621, einem Timing-Angriff auf den Sync-Login von Trilium Notes. Demonstriert die HMAC-Hash-Wiederherstellung über einen Netzwerk-Timing-Seitenkanal, mit Root-Cause, Fix und Minderungsdetails.
/api/login/sync in Trilium NotesSchweregrad: HIGH (CVSS 7.4) Betroffene Software: TriliumNext/Trilium < 0.101.0 Schwachstellentyp: CWE-208 – Beobachtbare Timing-Diskrepanz (Observable Timing Discrepancy) Behoben in: Trilium 0.101.0 (PR #8129) Veröffentlicht: 2026-02-06 | Reserviert: 2025-12-19
Trilium Notes ist eine quelloffene, plattformübergreifende, hierarchische Notizanwendung, die für den Aufbau großer persönlicher Wissensdatenbanken entwickelt wurde. Sie unterstützt:
Die Sync-Funktion ermöglicht es einem Trilium-Client, sich gegenüber einem Trilium-Server zu authentifizieren, damit Notizen geräteübergreifend synchron bleiben. Dieser Sync-Endpunkt ist der Einstiegspunkt für CVE-2025-68621.
Ein Timing-Angriff ist ein Seitenkanalangriff, bei dem ein Angreifer geheime Informationen erlangt, indem er misst, wie lange ein System für die Verarbeitung unterschiedlicher Eingaben benötigt.
Das klassische Beispiel ist der String-Vergleich:
"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
Die meisten Programmiersprachen vergleichen Strings Zeichen für Zeichen und brechen ab, sobald eine Abweichung gefunden wird (Early Exit). Das bedeutet:
Die Lösung besteht darin, eine Vergleichsfunktion mit konstanter Laufzeit (Constant-Time-Vergleich) zu verwenden, die immer jedes Byte prüft, unabhängig davon, wo eine Abweichung auftritt.
Die Schwachstelle wurde durch manuelle Code-Überprüfung der Authentifizierungslogik von Trilium entdeckt. Der Forscher untersuchte den Sync-Login-Ablauf in apps/server/src/routes/api/login.ts und bemerkte in der Funktion loginSync() (etwa in Zeile 111) das folgende Muster:
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..." }];
}
Ein Warnsignal ist die Verwendung des in JavaScript eingebauten Operators !== zum Vergleich von HMAC-Hashes. Der Operator !== ist nicht zeitkonstant — er bricht ab, sobald er ein abweichendes Zeichen findet. Da der Vergleich mit einfachen Strings durchgeführt wird (ohne eine kryptografisch sichere Vergleichsfunktion), leakt die Antwortzeit Informationen darüber, wie viele führende Bytes des Angreifer-Versuchs korrekt sind.
Der Forscher stellte dann die Frage:
„Kann dieser kleine Timing-Unterschied über ein Netzwerk stark genug verstärkt werden, um den vollständigen, 44 Zeichen langen Base64-codierten HMAC-Hash zu rekonstruieren?“
Die Antwort war, wie sich herausstellte, ja — mit genügend wiederholten Messungen und etwas statistischer Analyse hebt sich das Signal vom Rauschen ab.
Wenn ein Trilium-Client synchronisieren möchte, ruft er POST /api/login/sync mit einem JSON-Body wie diesem auf:
{
"timestamp": "2025-12-19T10:00:00.000Z",
"syncVersion": 34,
"hash": "<HMAC-SHA256 of documentSecret + timestamp, Base64-encoded>"
}
Die byteweise Wiederherstellung funktioniert wie folgt:
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
Nach 44 Iterationen (eine pro Base64-Zeichen) ist der vollständige, 44 Zeichen lange HMAC-Hash wiederhergestellt.
Praktische Anforderungen:
time.perf_counter() in Python bietet eine Auflösung im Nanosekundenbereich)Siehe poc.py für ein vollständig annotiertes Python-PoC.
Kurzübersicht, was das PoC tut:
A–Z, a–z, 0–9, +, /, =)./api/login/sync für jeden Kandidaten und misst die mittlere Antwortzeit (Median).Haftungsausschluss: Dieses PoC dient ausschließlich Bildungszwecken und verantwortungsvoller Sicherheitsforschung. Verwenden Sie es nicht gegen Systeme, die Sie nicht besitzen oder für die Sie keine ausdrückliche schriftliche Genehmigung zum Testen haben.
Die JavaScript-Operatoren !== (und ===) führen einen lexikografischen Vergleich mit frühem Abbruch durch. Die verwundbare Zeile in apps/server/src/routes/api/login.ts:
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
Das Verhalten des frühen Abbruchs erzeugt einen messbaren Timing-Unterschied pro übereinstimmendem Byte:
| Versuch vs. Erwartet | Verglichene Bytes | Zeit |
|---|---|---|
| Falsches Byte 0 | 1 | ~T |
| Korrektes Byte 0, falsches Byte 1 | 2 | ~T + δ |
| Korrekte Bytes 0–1, falsches Byte 2 | 3 | ~T + 2δ |
| … | … | … |
| Alle 44 Bytes korrekt | 44 | ~T + 43δ |
Jedes zusätzlich übereinstimmende Byte kostet eine winzige zusätzliche Menge an CPU-Zeit δ. Über Tausende von Stichproben hinweg ist die durchschnittliche Antwortzeit für einen Versuch mit „korrektem Byte N“ messbar länger als für einen Versuch mit „inkorrektem Byte N“, wodurch genügend Informationen preisgegeben werden, um den vollständigen HMAC-Hash Zeichen für Zeichen wiederherzustellen.
| Metrik | Wert | Begründung |
|---|---|---|
| Basis-Score | 7.4 HIGH | |
| Angriffsvektor | Netzwerk (N) | Über das Internet ausnutzbar |
| Angriffskomplexität | Hoch (H) | Erfordert viele Anfragen + stabiles Timing |
| Erforderliche Privilegien | Keine (N) | Kein Konto erforderlich |
| Benutzerinteraktion | Keine (N) | Das Opfer muss nichts tun |
| Auswirkungsbereich | Unverändert (U) | Nur der Trilium-Server ist betroffen |
| Vertraulichkeit | Hoch (H) | Die gesamte Notizdatenbank ist lesbar |
| Integrität | Hoch (H) | Der Angreifer kann Notizen schreiben/ändern |
| Verfügbarkeit | Keine (N) | Kein Denial-of-Service-Anteil |
Vector-String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Eine erfolgreiche Ausnutzung verschafft dem Angreifer:
Dies ist besonders schwerwiegend für Benutzer, die sensible persönliche Daten (Passwörter, private Dokumente, Tagebucheinträge) in ihrer Trilium-Wissensdatenbank speichern.
Die Behebung ersetzt den nicht zeitkonstanten Vergleich mit !== durch die in Node.js eingebaute Funktion crypto.timingSafeEqual():
Vorher (verwundbar):
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
Nachher (sicher):
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() vergleicht immer jedes Byte, sodass die Ausführungszeit nicht davon abhängt, wie viele Bytes übereinstimmen. Das Timing-Signal verschwindet.
Siehe vulnerable.ts und fix.ts für Code-Beispiele im direkten Vergleich.
Wenn Sie einen selbst gehosteten Trilium-Server betreiben, aktualisieren Sie sofort auf Version 0.101.0 oder höher.
# Docker example
docker pull zadam/trilium:0.101.0
Verwenden Sie niemals === / !==, um Geheimnisse zu vergleichen. Die Gleichheitsoperatoren von JavaScript sind nicht zeitkonstant. Jeder Vergleich von HMACs, Tokens oder Passwörtern mit === / !== ist ein potenzielles Timing-Orakel.
Verwenden Sie in Node.js immer crypto.timingSafeEqual() (oder ein Äquivalent in Ihrer Sprache/Laufzeitumgebung), wenn Sie kryptografische Werte vergleichen. Dies ist die standardmäßige, eigens für diese Aufgabe entwickelte API.
Timing-Angriffe sind über das Netzwerk real. Auch wenn Nanosekunden-Unterschiede über das Internet unmöglich zu erkennen scheinen, können statistische Verfahren und genügend Stichproben ein klares Signal aus verrauschten Messungen extrahieren — insbesondere in Umgebungen mit geringem Jitter.
Rate-Limiting allein ist keine ausreichende Abschwächung. Selbst mit Rate-Limiting pro IP kann ein Angreifer mit Zugriff auf rotierende Proxys oder ein Botnetz weiterhin genügend Stichproben sammeln, um den Timing-Unterschied auszunutzen.
Die HMAC-Verifizierung verdient dieselbe Sorgfalt wie der Passwortvergleich. HMAC-Hashes sind Geheimnisse. Behandeln Sie jeden Vergleich eines geheimen Werts so, als ob Timing-Seitenkanäle ausgenutzt werden könnten.
Die Code-Überprüfung kryptografischer Muster ist unerlässlich. Diese Schwachstelle wurde durch manuelle Prüfung gefunden — eine einzelne Codezeile, die harmlos aussah, aber schwerwiegende Sicherheitsimplikationen hatte. Dedizierte Krypto-/Sicherheitsaudits helfen, solche Probleme frühzeitig zu erkennen.
| Datum | Ereignis |
|---|---|
| 2025-12-19 | CVE-2025-68621 von GitHub Security reserviert |
| 2025-12-21 | Fix-PR #8129 eröffnet |
| 2025-12-25 | PR gemergt; Trilium 0.101.0 veröffentlicht |
| 2026-02-06 | CVE öffentlich publiziert |
| 2026-02-09 | CISA-ADP-Anreicherung hinzugefügt |
| Ressource | Link |
|---|---|
| GitHub-Sicherheitsempfehlung | GHSA-hxf6-58cx-qq3x |
| Fix-Pull-Request | TriliumNext/Trilium#8129 |
| CVE-Eintrag (CVEProject) | CVE-2025-68621.json |
| CWE-208 | Observable Timing Discrepancy |
| Trilium-Notes-Repository | TriliumNext/Trilium |
Dieses Repository wird zu Bildungs- und Forschungszwecken unter den Grundsätzen verantwortungsvoller Offenlegung gepflegt.