Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
HIGH severity

obsidian-local-rest-api: Authenticated path traversal via URL-encoded %2F in /vault/{path} — arbitrary host file read/write/deleteGHSA-62gx-5q78-wrvx

HIGH

GHSA-62gx-5q78-wrvx is a high-severity (CVSS 8.8) vulnerability in obsidian-local-rest-api. No vendor fix is recorded yet; mitigation options are listed below.

Published
Updated
Affected
1 pkg
Patched
None yet
Exploits
None indexed
Exploitation data as of Jul 15, 2026 · OSV.dev, FIRST.org (EPSS)

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

0other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
obsidian-local-rest-apinpm
278downloads / week

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.

Affected Packages

1 total
EcosystemPackageVulnerable rangeFix
📦npmobsidian-local-rest-apiall versionsNo fix

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for obsidian-local-rest-api, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Remediation status

    No patched version of obsidian-local-rest-api has shipped for GHSA-62gx-5q78-wrvx yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.

  3. Mitigate without a patch

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

Frequently Asked Questions

### 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 **arbitra
O3 Security · Impact-Aware SCA

Is GHSA-62gx-5q78-wrvx in your dependencies?

Find it across npm, including transitive dependencies.

obsidian-local-rest-api: Authenticated path traversal via…