
PoC didattico e analisi di CVE-2025-68621, un attacco di temporizzazione sul login di sincronizzazione di Trilium Notes. Dimostra il recupero dell'hash HMAC tramite canale laterale di temporizzazione di rete, con causa principale, correzione e dettagli di mitigazione.
/api/login/syncGravità: ALTA (CVSS 7.4) Software interessato: TriliumNext/Trilium < 0.101.0 Tipo di vulnerabilità: CWE-208 – Discrepanza temporale osservabile Corretta in: Trilium 0.101.0 (PR #8129) Pubblicata: 2026-02-06 | Riservata: 2025-12-19
Trilium Notes è un'applicazione open-source, multipiattaforma e gerarchica per prendere appunti, progettata per costruire grandi basi di conoscenza personali. Supporta:
La funzionalità di sincronizzazione consente a un client Trilium di autenticarsi su un server Trilium così che le note rimangano sincronizzate tra dispositivi. Questo endpoint di sincronizzazione è il punto di ingresso per CVE-2025-68621.
Un attacco timing è un attacco side-channel in cui un attaccante apprende informazioni segrete misurando quanto tempo impiega un sistema per elaborare input diversi.
L'esempio classico è il confronto tra stringhe:
"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
La maggior parte dei linguaggi di programmazione confronta le stringhe carattere per carattere e si ferma appena trova una discrepanza (uscita anticipata). Questo significa:
La soluzione è usare una funzione di confronto a tempo costante che ispezioni sempre ogni byte, indipendentemente da dove si verifica una discrepanza.
La vulnerabilità è stata scoperta tramite revisione manuale del codice della logica di autenticazione di Trilium. Il ricercatore ha esaminato il flusso di login di sincronizzazione in apps/server/src/routes/api/login.ts e ha notato il seguente schema nella funzione loginSync() (intorno alla riga 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..." }];
}
Il campanello d'allarme è l'uso dell'operatore !== integrato di JavaScript per confrontare gli hash HMAC. L'operatore !== non è a tempo costante — esce appena trova un carattere diverso. Poiché il confronto avviene su stringhe semplici (senza usare una funzione di confronto crittograficamente sicura), il tempo di risposta rivela informazioni su quanti byte iniziali dell'ipotesi dell'attaccante sono corretti.
Il ricercatore si è quindi chiesto:
"Questa piccola differenza di temporizzazione può essere amplificata abbastanza, attraverso una rete, da recuperare l'intero hash HMAC di 44 caratteri codificato in Base64?"
La risposta si è rivelata sì — con abbastanza misurazioni ripetute e un po' di analisi statistica, il segnale supera il rumore.
Quando un client Trilium vuole sincronizzarsi, chiama POST /api/login/sync con un body JSON come il seguente:
{
"timestamp": "2025-12-19T10:00:00.000Z",
"syncVersion": 34,
"hash": "<HMAC-SHA256 of documentSecret + timestamp, Base64-encoded>"
}
Il recupero byte per byte funziona come segue:
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
Dopo 44 iterazioni (una per carattere Base64), viene recuperato l'intero hash HMAC di 44 caratteri.
Requisiti pratici:
time.perf_counter() in Python offre una risoluzione in nanosecondi)Vedi poc.py per un PoC Python completamente annotato.
Breve riepilogo di ciò che fa il PoC:
A–Z, a–z, 0–9, +, /, =)./api/login/sync per ogni candidato e misura la mediana del tempo di risposta.Disclaimer: Questo PoC è fornito esclusivamente a scopo didattico e per ricerca sulla sicurezza responsabile. Non utilizzarlo contro sistemi che non possiedi o per i quali non hai esplicita autorizzazione scritta a testare.
Gli operatori !== (e ===) di JavaScript eseguono un confronto lessicografico con uscita anticipata. La riga vulnerabile in apps/server/src/routes/api/login.ts:
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
Il comportamento di uscita anticipata crea una differenza di temporizzazione misurabile per ogni byte corrispondente:
| Ipotesi vs. atteso | Byte confrontati | Tempo |
|---|---|---|
| Byte 0 errato | 1 | ~T |
| Byte 0 corretto, byte 1 errato | 2 | ~T + δ |
| Byte 0–1 corretti, byte 2 errato | 3 | ~T + 2δ |
| … | … | … |
| Tutti i 44 byte corretti | 44 | ~T + 43δ |
Ogni byte corrispondente aggiuntivo costa una piccolissima quantità extra di tempo CPU δ. Su migliaia di campioni, il tempo di risposta medio per un'ipotesi "byte N corretto" è misurabilmente più lungo di quello per un'ipotesi "byte N errato", rivelando abbastanza informazioni da recuperare l'intero hash HMAC carattere per carattere.
| Metrica | Valore | Motivo |
|---|---|---|
| Punteggio base | 7.4 ALTO | |
| Vettore di attacco | Rete (N) | Sfruttabile via internet |
| Complessità di attacco | Alta (H) | Richiede molte richieste + timing stabile |
| Privilegi richiesti | Nessuno (N) | Nessun account necessario |
| Interazione dell'utente | Nessuna (N) | La vittima non deve fare nulla |
| Ambito | Invariato (U) | È interessato solo il server Trilium |
| Riservatezza | Alta (H) | L'intera base di note è leggibile |
| Integrità | Alta (H) | L'attaccante può scrivere/modificare le note |
| Disponibilità | Nessuna (N) | Nessuna componente di denial-of-service |
Stringa del vettore: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Un exploit riuscito garantisce all'attaccante: