Skip to content

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

High severity GitHub Reviewed Published Jun 4, 2026 in coddingtonbear/obsidian-local-rest-api • Updated Jul 15, 2026

Package

npm obsidian-local-rest-api (npm)

Affected versions

< 4.1.3

Patched versions

None

Description

Summary

The Local REST API's /vault/{path} endpoints (GET/PUT/PATCH/POST/DELETE) percent-decode the request path inside the handler — after Express has already routed and normalized it, then hand it to the Obsidian vault adapter with no confinement check. A literal ../ is resolved/rejected at the routing layer (→ 404), but %2F is not a separator there, so ..%2F..%2F survives routing and is only turned into a real / by the handler's decodeURIComponent, reconstituting a ../ traversal that walks out of the vault. An authenticated client can read, write, or delete arbitrary files on the host with the Obsidian process's privileges.

Details

Framework: Express (import express from "express"; routes registered via this.api.route("/vault/*")…).

The vulnerable line — in src/requestHandler.ts, every vault handler (vaultGet, vaultPut, vaultPatch, vaultPost, vaultDelete) derives the path like this:

const rawPath = decodeURIComponent(
  req.path.slice(req.path.indexOf("/", 1) + 1),
);

The path is decodeURIComponent'd after Express routing. A literal ../ is collapsed/rejected at the routing layer, but %2F isn't a separator there — so ..%2F..%2F reaches the handler intact and this decodeURIComponent turns it into a real ../../. The string routing saw (…%2F…) is not the string the handler uses (…/…), and %2e%2e behaves the same way.

No confinement on the decoded path. The handlers pass rawPath straight to the vault adapter — e.g. this.app.vault.adapter.readBinary(filePath) / this.app.vault.getAbstractFileByPath(filePath) — with no path.resolve + vault-root prefix check, so the reconstituted ../../ escapes.

The fix already exists in your code — for MOVE only. vaultMove confines correctly:

const syntheticRoot = "/vault";
const resolved = posix.resolve(syntheticRoot, normalized);
if (resolved !== syntheticRoot && !resolved.startsWith(syntheticRoot + "/")) {
  this.returnCannedResponse(res, { errorCode: ErrorCode.PathTraversalNotAllowed });
  return;
}

GET/PUT/PATCH/POST/DELETE lack this guard. Apply the same posix.resolve(syntheticRoot, …) + startsWith check to the decoded path in every vault handler, and reject any segment that decodes to ...

PoC

Prereq: a running Obsidian with the Local REST API plugin enabled and its configured API key ($API_KEY). Targets below are Unix; adjust per OS (e.g. ..%2F..%2FWindows%2Fwin.ini on Windows).

# READ outside the vault (returns 200 + the target file's real bytes):
curl --path-as-is -k -H "Authorization: Bearer $API_KEY" \
  "https://127.0.0.1:27124/vault/..%2F..%2F..%2F..%2Fetc%2Fpasswd"

# WRITE outside the vault (creates a file on disk 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"

--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.

Impact

Authenticated arbitrary file read / write / delete outside the Obsidian vault, with the OS privileges of the Obsidian process — typically the user's home directory (SSH keys, browser profiles, dotfiles, credentials). Amplified in MCP/LLM-agent deployments: this API is widely used as an MCP server, so a prompt-injection in vault content (or a malicious MCP client) can make an agent emit a %2F path — turning "the agent can edit my notes" into "the agent can read/write any file on the host," with no user intent to grant filesystem access beyond the vault.

This vulnerability was reported by Caleb Brisbin through responsible disclosure.

References

Published to the GitHub Advisory Database Jul 15, 2026
Reviewed Jul 15, 2026
Last updated Jul 15, 2026

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

EPSS score

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

CVE ID

No known CVE

GHSA ID

GHSA-62gx-5q78-wrvx

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.