## https://sploitus.com/exploit?id=BC4124B4-98D5-5ED6-A16E-7E099EE2874D
# CVE-2025-68621 โ Trilium Notes Timing Attack on `/api/login/sync`
> **Severity:** HIGH (CVSS 7.4)
> **Affected Software:** TriliumNext/Trilium **Vulnerability Type:** CWE-208 โ Observable Timing Discrepancy
> **Fixed In:** Trilium 0.101.0 (PR [#8129](https://github.com/TriliumNext/Trilium/pull/8129))
> **Published:** 2026-02-06 | **Reserved:** 2025-12-19
---
## Table of Contents
1. [Overview](#overview)
2. [Background โ What Is Trilium Notes?](#background--what-is-trilium-notes)
3. [What Is a Timing Attack?](#what-is-a-timing-attack)
4. [How the Vulnerability Was Discovered](#how-the-vulnerability-was-discovered)
5. [Technical Deep Dive](#technical-deep-dive)
6. [CVSS Score Breakdown](#cvss-score-breakdown)
7. [Proof of Concept](#proof-of-concept)
8. [Impact](#impact)
9. [Patch / Fix](#patch--fix)
10. [Timeline](#timeline)
11. [References](#references)
---
## Overview
CVE-2025-68621 is a **timing attack vulnerability** in **Trilium Notes**, a popular open-source hierarchical note-taking application. The flaw lives in the server-side sync authentication endpoint (`/api/login/sync`) and allows an unauthenticated remote attacker to **recover an HMAC authentication hash one byte at a time** using statistical timing analysis โ without ever knowing the victim's password.
Once the hash is recovered, the attacker can authenticate to the sync endpoint and gain **full read/write access** to the victim's entire knowledge base.
---
## Background โ What Is Trilium Notes?
[Trilium Notes](https://github.com/TriliumNext/Trilium) is an open-source, cross-platform, hierarchical note-taking application designed for building large personal knowledge bases. It supports:
- A self-hosted server that multiple clients can sync with
- Rich note types (text, code, canvas, diagrams)
- A powerful scripting API
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.
---
## What Is a Timing Attack?
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:
- A guess that matches the first byte takes *a tiny bit longer* than one that mismatches immediately.
- By sending thousands of guesses and averaging response times, an attacker can statistically determine which byte is correct โ position by position โ until the full secret is recovered.
The fix is to use a **constant-time comparison** function that always inspects every byte regardless of where a mismatch occurs.
---
## How the Vulnerability Was Discovered
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):
```typescript
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.
---
## Technical Deep Dive
### The Authentication Flow
When a Trilium client wants to sync, it calls `POST /api/login/sync` with a JSON body like:
```json
{
"timestamp": "2025-12-19T10:00:00.000Z",
"syncVersion": 34,
"hash": ""
}
```
The server recomputes the expected HMAC and compares it to the submitted `hash`. If they match, sync is allowed.
### Why the Comparison Is Vulnerable
JavaScript's `!==` (and `===`) operators perform a **lexicographic, early-exit comparison**:
| 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ฮด |
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.
### The Attack Algorithm
```
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
- **>100 000 total HTTP requests** (50 samples ร 65 charset chars ร 44 positions โ 143 000)
- **>1 000 different source IP addresses** due to Trilium's rate-limiting (requires rotating proxies or a botnet)
- **Low network jitter** between attacker and server (LAN or stable cloud connection works best)
- A **high-precision timer** (`time.perf_counter()` in Python gives nanosecond resolution)
---
## CVSS Score Breakdown
| 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 |
**Vector String:** `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N`
---
## Proof of Concept
See [`poc.py`](./poc.py) for a fully annotated Python PoC.
**Quick summary of what the PoC does:**
1. Iterates through all 44 Base64 character positions of the HMAC hash.
2. For each position, tries every character in the Base64 charset (`AโZ`, `aโz`, `0โ9`, `+`, `/`, `=`).
3. Sends 50 HTTP POST requests to `/api/login/sync` for each candidate and measures the median response time.
4. Selects the candidate with the highest median response time as the correct character.
5. After recovering all 44 characters, authenticates with the recovered hash.
> **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.
---
## Impact
A successful exploit gives the attacker:
- **Complete read access** to all notes, including encrypted note metadata
- **Complete write access** โ the attacker can create, modify, or delete notes
- **Persistent access** โ the recovered hash can be reused (within the timestamp window)
This is especially severe for users who store sensitive personal data (passwords, private documents, journal entries) in their Trilium knowledge base.
---
## Patch / Fix
The fix replaces the non-constant-time `!==` comparison with Node.js's built-in `crypto.timingSafeEqual()`:
**Before (vulnerable):**
```typescript
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
```
**After (secure):**
```typescript
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`](./vulnerable.ts) and [`fix.ts`](./fix.ts) for side-by-side code examples.
### How to Update
If you are running a self-hosted Trilium server, upgrade to **version 0.101.0 or later** immediately.
```bash
# Docker example
docker pull zadam/trilium:0.101.0
```
---
## Timeline
| Date | Event |
|------|-------|
| 2025-12-19 | CVE-2025-68621 reserved by GitHub Security |
| 2025-12-21 | Fix PR [#8129](https://github.com/TriliumNext/Trilium/pull/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 |
---
## References
| Resource | Link |
|----------|------|
| GitHub Security Advisory | [GHSA-hxf6-58cx-qq3x](https://github.com/TriliumNext/Trilium/security/advisories/GHSA-hxf6-58cx-qq3x) |
| Fix Pull Request | [TriliumNext/Trilium#8129](https://github.com/TriliumNext/Trilium/pull/8129) |
| CVE Record (CVEProject) | [CVE-2025-68621.json](https://github.com/CVEProject/cvelistV5/blob/main/cves/2025/68xxx/CVE-2025-68621.json) |
| CWE-208 | [Observable Timing Discrepancy](https://cwe.mitre.org/data/definitions/208.html) |
| Trilium Notes Repository | [TriliumNext/Trilium](https://github.com/TriliumNext/Trilium) |
---
*This repository is maintained for educational and research purposes under responsible disclosure principles.*