
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.