
CVE-2025-68621에 대한 교육용 PoC 및 분석, Trilium Notes 동기화 로그인에 대한 타이밍 공격입니다. 네트워크 타이밍 사이드 채널을 통한 HMAC 해시 복구를 시연하며, 근본 원인, 수정 사항 및 완화 방법에 대한 세부 정보를 포함합니다.
/api/login/sync심각도: HIGH (CVSS 7.4) 영향받는 소프트웨어: TriliumNext/Trilium < 0.101.0 취약점 유형: CWE-208 – 관찰 가능한 타이밍 차이 수정 버전: Trilium 0.101.0 (PR #8129) 공개일: 2026-02-06 | 예약일: 2025-12-19
Trilium Notes는 대규모 개인 지식 기반 구축을 위해 설계된 오픈 소스, 크로스 플랫폼, 계층형 메모 애플리케이션입니다. 다음을 지원합니다:
동기화 기능을 통해 Trilium 클라이언트가 Trilium 서버에 인증하여 노트를 여러 장치에서 동기화 상태로 유지합니다. 이 동기화 엔드포인트가 CVE-2025-68621의 진입점입니다.
타이밍 공격은 공격자가 시스템이 다양한 입력을 처리하는 데 걸리는 시간을 측정하여 비밀 정보를 알아내는 부채널 공격입니다.
전형적인 예는 문자열 비교입니다:
"correct_password" !== "aorrect_password" → 0번째 위치에서 실패 → 빠름
"correct_password" !== "cXrrect_password" → 1번째 위치에서 실패 → 약간 느림
"correct_password" !== "correct_password" → 완전 일치 → 가장 느림
대부분의 프로그래밍 언어는 문자열을 문자별로 비교하고 불일치가 발견되는 즉시 중단합니다 (조기 종료). 즉:
수정 방법은 불일치가 발생하는 위치와 관계없이 항상 모든 바이트를 검사하는 상수 시간 비교 함수를 사용하는 것입니다.
이 취약점은 Trilium의 인증 로직에 대한 수동 코드 검토를 통해 발견되었습니다. 연구원은 apps/server/src/routes/api/login.ts의 동기화 로그인 흐름을 살펴보고 loginSync() 함수(약 111번째 줄)에서 다음과 같은 패턴을 발견했습니다:
const documentSecret = options.getOption("documentSecret");
const expectedHash = utils.hmac(documentSecret, timestampStr);
const givenHash = req.body.hash;
if (expectedHash !== givenHash) { // ← 취약한 줄
return [400, { message: "Sync login credentials are incorrect..." }];
}
위험 신호는 JavaScript의 기본 !== 연산자를 HMAC 해시 비교에 사용한다는 점입니다. !== 연산자는 상수 시간이 아닙니다 — 다른 문자를 발견하는 즉시 종료됩니다. 비교가 일반 문자열(암호학적으로 안전한 비교 함수 사용하지 않음)에서 이루어지기 때문에 응답 시간을 통해 공격자의 추측 중 몇 바이트가 올바른지에 대한 정보가 유출됩니다.
연구원은 다음과 같이 질문했습니다:
"이 작은 타이밍 차이를 네트워크를 통해 충분히 증폭시켜 전체 44자 Base64 인코딩 HMAC 해시를 복구할 수 있을까?"
답은 예였습니다 — 충분한 반복 측정과 통계 분석을 통해 신호가 노이즈 위로 올라옵니다.
Trilium 클라이언트가 동기화하려면 다음과 같은 JSON 본문으로 POST /api/login/sync를 호출합니다:
{
"timestamp": "2025-12-19T10:00:00.000Z",
"syncVersion": 34,
"hash": "<HMAC-SHA256 of documentSecret + timestamp, Base64-encoded>"
}
바이트 단위 복구는 다음과 같이 작동합니다:
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
44회 반복(Base64 문자당 한 번) 후 전체 44자 HMAC 해시가 복구됩니다.
실제 요구 사항:
time.perf_counter()는 나노초 해상도 제공)완전히 주석 처리된 Python PoC는 poc.py를 참조하세요.
PoC가 수행하는 작업에 대한 간략한 요약:
A–Z, a–z, 0–9, +, /, =)의 모든 문자를 시도합니다./api/login/sync로 보내고 중간 응답 시간을 측정합니다.면책 조항: 이 PoC는 교육 목적과 책임 있는 보안 연구를 위해서만 제공됩니다. 소유하지 않거나 명시적 서면 허가를 받지 않은 시스템에 대해 사용하지 마십시오.
JavaScript의 !== (그리고 ===) 연산자는 사전식, 조기 종료 비교를 수행합니다. apps/server/src/routes/api/login.ts의 취약한 줄:
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
조기 종료 동작으로 인해 일치하는 바이트당 측정 가능한 타이밍 차이가 발생합니다:
| 추측 대 예상 | 비교된 바이트 수 | 시간 |
|---|---|---|
| 0번째 바이트 오류 | 1 | ~T |
| 0번째 바이트 정확, 1번째 바이트 오류 | 2 | ~T + δ |
| 0-1번째 바이트 정확, 2번째 바이트 오류 | 3 | ~T + 2δ |
| … | … | … |
| 44개 바이트 모두 정확 | 44 | ~T + 43δ |
일치하는 바이트가 추가될 때마다 약간의 추가 CPU 시간 δ가 소모됩니다. 수천 개의 샘플을 통해 "올바른 바이트 N" 추측의 평균 응답 시간이 "잘못된 바이트 N" 추측보다 측정 가능하게 길어지며, 이는 전체 HMAC 해시를 한 문자씩 복구할 수 있을 만큼 정보를 유출합니다.
| 지표 | 값 | 이유 |
|---|---|---|
| 기본 점수 | 7.4 HIGH | |
| 공격 벡터 | Network (N) | 인터넷을 통해 악용 가능 |
| 공격 복잡성 | High (H) | 많은 요청 + 안정적인 타이밍 필요 |
| 필요한 권한 | None (N) | 계정 불필요 |
| 사용자 상호작용 | None (N) | 피해자가 아무것도 할 필요 없음 |
| 범위 | Unchanged (U) | Trilium 서버만 영향받음 |
| 기밀성 | High (H) | 전체 노트 베이스 읽기 가능 |
| 무결성 | High (H) | 공격자가 노트 생성/수정 가능 |
| 가용성 | None (N) | 서비스 거부 요소 없음 |
벡터 문자열: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
성공적인 악용 시 공격자는 다음을 얻습니다:
이는 Trilium 지식 베이스에 민감한 개인 데이터(비밀번호, 개인 문서, 일기 항목)를 저장하는 사용자에게 특히 심각합니다.
수정은 상수 시간이 아닌 !== 비교를 Node.js 내장 crypto.timingSafeEqual()로 대체합니다:
수정 전 (취약):
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
수정 후 (안전):
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()는 항상 모든 바이트를 비교하므로 실행 시간이 일치하는 바이트 수에 의존하지 않습니다. 타이밍 신호가 사라집니다.
병렬 코드 예제는 vulnerable.ts와 fix.ts를 참조하세요.
자체 호스팅 Trilium 서버를 실행 중인 경우 즉시 버전 0.101.0 이상으로 업그레이드하십시오.
# Docker 예제
docker pull zadam/trilium:0.101.0
비밀 비교에 === / !==를 절대 사용하지 마십시오. JavaScript의 동등 연산자는 상수 시간이 아닙니다. === / !==를 사용한 HMAC, 토큰 또는 비밀번호의 모든 비교는 잠재적인 타이밍 오라클입니다.
Node.js에서는 항상 crypto.timingSafeEqual()을 사용하십시오 (또는 해당 언어/런타임의 동등한 함수). 이 작업을 위해 특별히 설계된 표준 API입니다.
타이밍 공격은 네트워크를 통해 실현 가능합니다. 나노초 차이가 인터넷을 통해 감지 불가능해 보일 수 있지만, 통계 기술과 충분한 샘플은 잡음이 많은 측정에서도 명확한 신호를 추출할 수 있습니다 — 특히 지터가 낮은 환경에서 더욱 그렇습니다.
속도 제한만으로는 충분한 완화책이 아닙니다. IP당 속도 제한이 있더라도 로테이팅 프록시나 봇넷에 접근할 수 있는 공격자는 타이밍 차이를 악용할 만큼 충분한 샘플을 축적할 수 있습니다.
HMAC 검증은 비밀번호 비교와 동일한 주의를 기울여야 합니다. HMAC 해시는 비밀입니다. 비밀 값의 모든 비교는 타이밍 부채널이 악용될 수 있는 것으로 취급하십시오.
암호화 패턴에 대한 코드 검토는 필수입니다. 이 취약점은 수동 검토를 통해 발견되었습니다 — 단 한 줄의 코드가 무해해 보였지만 심각한 보안 영향을 가졌습니다. 전용 암호화/보안 감사는 이러한 문제를 조기에 발견하는 데 도움이 됩니다.
| 날짜 | 이벤트 |
|---|---|
| 2025-12-19 | CVE-2025-68621이 GitHub Security에 의해 예약됨 |
| 2025-12-21 | 수정 PR #8129 열림 |
| 2025-12-25 | PR 병합; Trilium 0.101.0 출시 |
| 2026-02-06 | CVE 공개 |
| 2026-02-09 | CISA ADP 강화 추가됨 |
| 자료 | 링크 |
|---|---|
| GitHub Security Advisory | GHSA-hxf6-58cx-qq3x |
| 수정 Pull Request | TriliumNext/Trilium#8129 |
| CVE 레코드 (CVEProject) | CVE-2025-68621.json |
| CWE-208 | 관찰 가능한 타이밍 불일치 |
| Trilium Notes 저장소 | TriliumNext/Trilium |
이 저장소는 책임 있는 공개 원칙에 따라 교육 및 연구 목적으로 유지됩니다.