high · 8.8Jul 15, 2026

obsidian-local-rest-api: Authenticated Path Traversal via Percent-Encoded Slash in /vault/{path}

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A bug in the Obsidian Local REST API plugin lets an authenticated caller read, write, or delete any file on the host machine by sneaking encoded slashes (%2F) past the URL router and into the file pat

Packageobsidian-local-rest-api
Ecosystemnpm
Affected< 4.1.3

The problem

Every `/vault/{path}` endpoint (GET, PUT, PATCH, POST, DELETE) calls `decodeURIComponent` on the path segment inside the handler, after Express has already routed the request. Express sees `%2F` as a literal percent-encoded character, not a separator, so it does not collapse or reject `..%2F` sequences during routing.

Once the handler decodes the path, `..%2F..%2F` becomes `../../`, which walks outside the vault root. The decoded path is passed directly to Obsidian vault adapter calls (`readBinary`, `getAbstractFileByPath`, etc.) with no `posix.resolve` confinement check, giving an authenticated client arbitrary file read, write, and delete at the OS privileges of the Obsidian process.

Proof of concept

A working proof-of-concept for this issue in obsidian-local-rest-api, with the exact payload below.

bash
# READ: returns /etc/passwd bytes with HTTP 200
curl --path-as-is -k \
  -H "Authorization: Bearer $API_KEY" \
  "https://127.0.0.1:27124/vault/..%2F..%2F..%2F..%2Fetc%2Fpasswd"

# WRITE: creates a file outside the vault root
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"

# NOTE: a literal ../ (unencoded) returns 404 -- only the %2F form bypasses routing.

The root cause is a decode-after-routing gap (CWE-22). The string Express routes on (`..%2F..%2F`) is not the string the handler operates on (`../../`), so the router's path-normalization never sees the traversal.

The fix, already present in `vaultMove` and now applied to all vault handlers, resolves the decoded path against a synthetic vault root using `posix.resolve(syntheticRoot, decoded)` and rejects any result that does not start with `syntheticRoot + '/'`. This makes traversal impossible regardless of encoding variant (`%2F`, `%2e%2e`, or combinations).

The fix

Upgrade obsidian-local-rest-api to 4.1.3 or later. The patch applies a `posix.resolve` confinement check to every vault handler (GET, PUT, PATCH, POST, DELETE, MOVE) and returns `ErrorCode.PathTraversalNotAllowed` on any path that resolves outside the vault root.

No configuration change is required after upgrading.

Reported by Caleb Brisbin (@AgenticWizard).

References: [1][2][3]

Related research