Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2025-68621 | Kitploit
Tools/GitHubGitHub/sivaadityacoder/cve-2025-68621
Vulnerability AnalysisExploitationWeb SecurityCryptographyPapers & ResearchLearning & Education
GitHubsivaadityacoder/cve-2025-68621

CVE-2025-68621

View Repository
3 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2025-68621 — Trilium Notes Timing Attack on /api/login/sync

Severity: 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


Table of Contents

  1. My Approach
  2. Root Cause
  3. Impact
  4. Fix
  5. Key Takeaways
  6. Timeline
  7. References

My Approach

What Is Trilium Notes?

Trilium Notes 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:

root@kitploit:~
"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):

root@kitploit:~
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.

The Attack Algorithm

When a Trilium client wants to sync, it calls POST /api/login/sync with a JSON body like:

root@kitploit:~
{
  "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:

root@kitploit:~
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)

Proof of Concept

See 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.


Root Cause

JavaScript's !== (and ===) operators perform a lexicographic, early-exit comparison. The vulnerable line in apps/server/src/routes/api/login.ts:

root@kitploit:~
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.

CVSS Score Breakdown

Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N


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.


Fix

The fix replaces the non-constant-time !== comparison with Node.js's built-in crypto.timingSafeEqual():

Before (vulnerable):

root@kitploit:~
if (expectedHash !== givenHash) {
    return [400, { message: "Sync login credentials are incorrect..." }];
}

After (secure):

root@kitploit:~
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.

How to Update

If you are running a self-hosted Trilium server, upgrade to version 0.101.0 or later immediately.

root@kitploit:~
# Docker example
docker pull zadam/trilium:0.101.0

Key Takeaways

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.


Timeline


References


This repository is maintained for educational and research purposes under responsible disclosure principles.

Download Tool
Guess vs. ExpectedBytes comparedTime
Wrong byte 01~T
Correct byte 0, wrong byte 12~T + δ
Correct bytes 0–1, wrong byte 23~T + 2δ
………
All 44 bytes correct44~T + 43δ
MetricValueReason
Base Score7.4 HIGH
Attack VectorNetwork (N)Exploitable over the internet
Attack ComplexityHigh (H)Requires many requests + stable timing
Privileges RequiredNone (N)No account needed
User InteractionNone (N)Victim does not need to do anything
ScopeUnchanged (U)Only the Trilium server is affected
ConfidentialityHigh (H)Full note base is readable
IntegrityHigh (H)Attacker can write/modify notes
AvailabilityNone (N)No denial-of-service component
Date
Event
2025-12-19CVE-2025-68621 reserved by GitHub Security
2025-12-21Fix PR #8129 opened
2025-12-25PR merged; Trilium 0.101.0 released
2026-02-06CVE publicly published
2026-02-09CISA ADP enrichment added
ResourceLink
GitHub Security AdvisoryGHSA-hxf6-58cx-qq3x
Fix Pull RequestTriliumNext/Trilium#8129
CVE Record (CVEProject)CVE-2025-68621.json
CWE-208Observable Timing Discrepancy
Trilium Notes RepositoryTriliumNext/Trilium