
/api/login/syncSeverity: HIGH (CVSS 7.4) Affected Software: TriliumNext/Trilium < 0.101.0 Vulnerability Type: CWE-208 – Observable Timing Discrepancy Fixed In: Trilium 0.101.0 (PR #8129) Published: 2026-02-06 | Reserved: 2025-12-19
Trilium Notes is an open-source, cross-platform, hierarchical note-taking application designed for building large personal knowledge bases. It supports:
The sync feature lets a Trilium client authenticate to a Trilium server so notes stay in sync across devices. This sync endpoint is the entry point for CVE-2025-68621.
A timing attack is a side-channel attack where an attacker learns secret information by measuring how long a system takes to process different inputs.
The classic example is string comparison:
"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
Most programming languages compare strings character by character and stop as soon as a mismatch is found (early exit). This means:
The fix is to use a constant-time comparison function that always inspects every byte regardless of where a mismatch occurs.
The vulnerability was discovered through manual code review of Trilium's authentication logic. The researcher examined the sync login flow in apps/server/src/routes/api/login.ts and noticed the following pattern in the loginSync() function (around line 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..." }];
}
The red flag is the use of JavaScript's built-in !== operator for comparing HMAC hashes. The !== operator is not constant-time — it exits as soon as it finds a differing character. Because the comparison is done on plain strings (not using a cryptographically safe comparison function), response time leaks information about how many leading bytes of the attacker's guess are correct.
The researcher then asked:
"Can this small timing difference be amplified enough, across a network, to recover the full 44-character Base64-encoded HMAC hash?"
The answer turned out to be yes — with enough repeated measurements and some statistical analysis, the signal rises above the noise.
When a Trilium client wants to sync, it calls POST /api/login/sync with a JSON body like:
{
"timestamp": "2025-12-19T10:00:00.000Z",
"syncVersion": 34,
"hash": "<HMAC-SHA256 of documentSecret + timestamp, Base64-encoded>"
}
The byte-by-byte recovery works as follows:
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
After 44 iterations (one per Base64 character), the full 44-character HMAC hash is recovered.
Practical requirements:
time.perf_counter() in Python gives nanosecond resolution)See poc.py for a fully annotated Python PoC.
Quick summary of what the PoC does:
A–Z, a–z, 0–9, +, /, =)./api/login/sync for each candidate and measures the median response time.Disclaimer: This PoC is provided for educational purposes and responsible security research only. Do not use against systems you do not own or have explicit written permission to test.
JavaScript's !== (and ===) operators perform a lexicographic, early-exit comparison. The vulnerable line in apps/server/src/routes/api/login.ts:
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
The early-exit behaviour creates a measurable timing difference per matching byte:
Each additional matching byte costs a tiny extra amount of CPU time δ. Over thousands of samples, the average response time for a "correct byte N" guess is measurably longer than for an "incorrect byte N" guess, leaking enough information to recover the full HMAC hash character by character.
Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
A successful exploit gives the attacker:
This is especially severe for users who store sensitive personal data (passwords, private documents, journal entries) in their Trilium knowledge base.
The fix replaces the non-constant-time !== comparison with Node.js's built-in crypto.timingSafeEqual():
Before (vulnerable):
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
After (secure):
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() always compares every byte, so the execution time does not depend on how many bytes match. The timing signal disappears.
See vulnerable.ts and fix.ts for side-by-side code examples.
If you are running a self-hosted Trilium server, upgrade to version 0.101.0 or later immediately.
# Docker example
docker pull zadam/trilium:0.101.0
Never use === / !== to compare secrets. JavaScript's equality operators are not constant-time. Any comparison of HMACs, tokens, or passwords using === / !== is a potential timing oracle.
Always use crypto.timingSafeEqual() in Node.js (or an equivalent in your language/runtime) when comparing cryptographic values. This is the standard, purpose-built API for this task.
Timing attacks are real over the network. While nanosecond differences seem impossible to detect across the internet, statistical techniques and enough samples can extract a clear signal from noisy measurements — especially in low-jitter environments.
Rate-limiting alone is not sufficient mitigation. Even with rate-limiting per IP, an attacker with access to rotating proxies or a botnet can still accumulate enough samples to exploit the timing difference.
HMAC verification deserves the same care as password comparison. HMAC hashes are secrets. Treat any comparison of a secret value as if timing side channels could be exploited.
Code review for cryptographic patterns is essential. This vulnerability was found through manual review — a single line of code that looked innocuous but had serious security implications. Dedicated crypto/security audits help catch these issues early.
This repository is maintained for educational and research purposes under responsible disclosure principles.
| Guess vs. Expected | Bytes compared | Time |
|---|
| Wrong byte 0 | 1 | ~T |
| Correct byte 0, wrong byte 1 | 2 | ~T + δ |
| Correct bytes 0–1, wrong byte 2 | 3 | ~T + 2δ |
| … | … | … |
| All 44 bytes correct | 44 | ~T + 43δ |
| Metric | Value | Reason |
|---|
| Base Score | 7.4 HIGH | |
| Attack Vector | Network (N) | Exploitable over the internet |
| Attack Complexity | High (H) | Requires many requests + stable timing |
| Privileges Required | None (N) | No account needed |
| User Interaction | None (N) | Victim does not need to do anything |
| Scope | Unchanged (U) | Only the Trilium server is affected |
| Confidentiality | High (H) | Full note base is readable |
| Integrity | High (H) | Attacker can write/modify notes |
| Availability | None (N) | No denial-of-service component |
| Date |
|---|
| Event |
|---|
| 2025-12-19 | CVE-2025-68621 reserved by GitHub Security |
| 2025-12-21 | Fix PR #8129 opened |
| 2025-12-25 | PR merged; Trilium 0.101.0 released |
| 2026-02-06 | CVE publicly published |
| 2026-02-09 | CISA ADP enrichment added |
| Resource | Link |
|---|
| GitHub Security Advisory | GHSA-hxf6-58cx-qq3x |
| Fix Pull Request | TriliumNext/Trilium#8129 |
| CVE Record (CVEProject) | CVE-2025-68621.json |
| CWE-208 | Observable Timing Discrepancy |
| Trilium Notes Repository | TriliumNext/Trilium |