---
id: "GHSA-62gx-5q78-wrvx"
kind: "vulnerability"
aliases: ["GHSA-62gx-5q78-wrvx"]
title: "obsidian-local-rest-api: Authenticated path traversal via URL-encoded %2F in /vault/{path} \u2014 arbitrary host file read/write/delete"
severity: "high"
cvss: 8.8
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "obsidian-local-rest-api", "fixed": ["4.1.3"], "affected": "< 4.1.3", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["claude-code", "cwe-22", "mcp", "obsidian", "obsidian-local-rest-api", "package", "self-hosted"]
not_affected_if: ["api-disabled", "plugin-not-installed", "version-4.1.3-or-later"]
published_at: "2026-07-15T21:56:45+00:00"
first_seen_at: "2026-11-02T19:28:00+00:00"
updated_at: "2026-11-02T19:28:41+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-62gx-5q78-wrvx", "first_seen_at": "2026-11-02T19:28:00+00:00"}]
---

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

The Obsidian Local REST API plugin decodes vault paths after routing, so a %2F sequence becomes a directory traversal that escapes the vault. An authenticated client can read, write or delete any file the Obsidian process can reach on the host.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:obsidian-local-rest-api affected < 4.1.3 fixed 4.1.3

## How to check

- Check the installed plugin version in the vault's `.obsidian/plugins/obsidian-local-rest-api/manifest.json` (the `version` field) or with `npm ls obsidian-local-rest-api` if it is used as a package.
- Confirm whether the REST API is enabled in Obsidian settings under the Local REST API plugin.
- Check whether an MCP server or agent is configured to call the `/vault/` endpoints.

## Fix

- obsidian-local-rest-api: move to `4.1.3` or later

## Timeline

- 2026-11-02T19:28:00+00:00 ghsa https://github.com/advisories/GHSA-62gx-5q78-wrvx

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/coddingtonbear/obsidian-local-rest-api/security-advisories/GHSA-62gx-5q78-wrvx
- [advisory] Maintainer's advisory: github.com https://github.com/coddingtonbear/obsidian-local-rest-api/security/advisories/GHSA-62gx-5q78-wrvx
- [fix] Release: github.com https://github.com/coddingtonbear/obsidian-local-rest-api/releases/tag/4.1.3
- [fix] Patch: github.com https://github.com/coddingtonbear/obsidian-local-rest-api/commit/fdc97f9e652e0d38cb43911d31f827a12b248fc8

## From the sources

### 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:
  ```ts
  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:
  ```ts
  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).

  ```bash
  # 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.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
