obsidian-local-rest-api: /vault/{path}의 URL 인코딩 %2F를 통해 인증된 경로 탐색 — 임의 호스트 파일 읽기/쓰기/삭제
로컬 REST API의 /vault/{path} 엔드포인트(GET/PUT/PATCH/POST/DELETE)는 핸들러 내부에서 요청 경로를 퍼센트 디코딩합니다. 이후* Express는 이를 이미 라우팅하고 정규화한 후 제한 확인 없이 Obsidian 볼트 어댑터에 전달합니다. 리터럴 ../는 라우팅 계층에서 해결/거부되지만(→ 404) %2F는 구분 기호가 아니므로 ..%2F..%2F는 라우팅에서 살아남고 핸들러의 decodeURIComponent에 의해 실제 /로 변환되어 저장소에서 나가는 ../ 순회를 재구성합니다. 인증된 클라이언트는 Obsidian 프로세스의 권한으로 호스트의 임의 파일을 읽고, 쓰고, 삭제할 수 있습니다.
Framework: Express (import express from "express"; routes registered via this.api.route("/vault/*")…).
취약한 라인 — src/requestHandler.ts에서 모든 Vault 핸들러(vaultGet, vaultPut, vaultPatch, vaultPost, vaultDelete)는 다음과 같은 경로를 파생합니다.
``ts
const rawPath = decodeURIComponent(
req.path.slice(req.path.indexOf("/", 1) + 1),
);
The path is `decodeURIComponent`'d **after** Express routing. 리터럴 `../`은 라우팅 계층에서 축소/거부되지만 `%2F`는 구분 기호가 아닙니다. 따라서 `..%2F..%2F`는 핸들러에 그대로 도달하고 이 `decodeURIComponent`는 이를 실제 `../../`로 바꿉니다. **문자열 라우팅 톱(`…%2F…`)은 처리기가 사용하는 문자열(`…/…`)이 아니며 `%2e%2e`는 동일한 방식으로 동작합니다.
**디코딩된 경로에는 제한이 없습니다.** 핸들러는 'rawPath'를 볼트 어댑터에 직접 전달합니다. `this.app.vault.adapter.readBinary(filePath)` / `this.app.vault.getAbstractFileByPath(filePath)` — `path.resolve` + 볼트 루트 접두사 확인이 없으므로 재구성된 `../../`가 이스케이프됩니다.
**수정 사항은 코드에 이미 존재합니다. MOVE에만 적용됩니다.** `vaultMove`는 올바르게 제한됩니다.
``ts
const SyntheticRoot = "/vault";
const 해결 = posix.resolve(syntheticRoot, 정규화);
if (해결됨 !== SyntheticRoot && !resolved.startsWith(syntheticRoot + "/")) {
this.returnCannedResponse(res, { errorCode: ErrorCode.PathTraversalNotAllowed });
반품;
}
GET/PUT/PATCH/POST/DELETE에는 이 가드가 없습니다. **모든 볼트 핸들러의 디코딩된 경로에 동일한 posix.resolve(syntheticRoot, …) + startsWith 검사를 적용하고 ..로 디코딩되는 모든 세그먼트를 거부합니다.
전제조건: 로컬 REST API 플러그인이 활성화되고 API 키($API_KEY)가 구성된 Obsidian을 실행해야 합니다. 아래 대상은 Unix입니다. OS별로 조정합니다(예: Windows의 경우 ..%2F..%2FWindows%2Fwin.ini).
``배쉬
curl --path-as-is -k -H "Authorization: Bearer $API_KEY"
"https://127.0.0.1:27124/vault/..%2F..%2F..%2F..%2Fetc%2Fpasswd"
curl --path-as-is -k -X PUT -H "Authorization: Bearer $API_KEY" --data "pwned"
"https://127.0.0.1:27124/vault/..%2F..%2F..%2Ftmp%2Fcanary.txt"
`--path-as-is` stops curl from collapsing `..` client-side. A plain `../` (unencoded) request returns 404 — only the `%2F`/`%2e%2e` encoded form bypasses, confirming the decode-after-routing gap.
### 영향
Obsidian 프로세스의 OS 권한을 사용하여 Obsidian 저장소 외부에서 인증된 임의 파일 **읽기/쓰기/삭제**(일반적으로 사용자의 홈 디렉터리(SSH 키, 브라우저 프로필, 도트 파일, 자격 증명)). MCP/LLM 에이전트 배포에서 증폭됨: 이 API는 MCP 서버로 널리 사용되므로 볼트 콘텐츠(또는 악의적인 MCP 클라이언트)에 프롬프트 삽입을 하면 에이전트가 '%2F' 경로를 내보낼 수 있습니다. 즉, 볼트 너머의 파일 시스템 액세스 권한을 부여하려는 사용자 의도 없이 "에이전트가 내 메모를 편집할 수 있음"을 "에이전트가 호스트의 모든 파일을 읽고 쓸 수 있음"으로 바꿀 수 있습니다.
이 취약점은 Caleb Brisbin이 책임 있는 공개를 통해 보고했습니다.
왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 88.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 88.00 |
VPI 공식 vpi-v1 기준