Skip to content

Instantly share code, notes, and snippets.

@spdc-elm
Last active February 12, 2026 03:01
Show Gist options
  • Select an option

  • Save spdc-elm/b059904a9e351afc1f5d38351f8b9ea5 to your computer and use it in GitHub Desktop.

Select an option

Save spdc-elm/b059904a9e351afc1f5d38351f8b9ea5 to your computer and use it in GitHub Desktop.

Vulnerability: Path Traversal in knowns MCP create_doc tool (.md File Write Outside Intended Directory)

Vulnerable project: https://github.com/knowns-dev/knowns

Affected Version

Vulnerability Class

  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory (Path Traversal)
  • Impact: .md file write and arbitrary directory creation outside the intended .knowns/docs/ directory

Severity

  • Severity: Medium
  • Risk: Medium
  • Note: Impact is significantly mitigated by the fact that the file extension is hardcoded to .md and the filename is sanitized to [a-z0-9-] only. This is NOT arbitrary file write — the attacker cannot control the file extension or write non-markdown files.

Affected Component / Attack Surface

  • Service: knowns MCP Server (stdio transport)
  • Tool/Action: create_doc
  • Vulnerable parameter: folder
  • Entry point: MCP CallToolRequest via stdio

Preconditions / Threat Model

This issue is exploitable under either of the following common deployment models:

  1. Direct MCP client access

    • The knowns MCP server is running and an MCP client (e.g., Claude Desktop, Cursor, or any MCP-compatible IDE) is connected via stdio.
    • An attacker with access to the MCP client can invoke create_doc with a malicious folder parameter.
  2. Indirect invocation via an AI agent (prompt injection / tool abuse)

    • knowns is integrated as an MCP tool provider for an LLM-based agent.
    • An attacker can coerce the AI agent into calling create_doc with a crafted folder value containing path traversal sequences, resulting in arbitrary file writes outside the intended directory.

Technical Description

The create_doc MCP tool handler accepts a folder parameter to specify a subdirectory under .knowns/docs/ where the new document should be created. The handler only strips leading and trailing slashes from folder but does not sanitize or reject ../ path traversal sequences:

if (input.folder) {
    const folderPath = input.folder.replace(/^\/|\/$/g, "");  // Only strips leading/trailing slashes
    targetDir = join(getDocsDir(), folderPath);                // "../../../" is NOT filtered
    relativePath = join(folderPath, filename);
    if (!existsSync(targetDir)) {
        await mkdir(targetDir, { recursive: true });           // Creates arbitrary directories
    }
}
const filepath = join(targetDir, filename);
await writeFile(filepath, fileContent, "utf-8");               // Writes to arbitrary location

The title parameter is safely sanitized by titleToFilename() (which strips all non-alphanumeric characters and replaces them with hyphens), and the file extension is hardcoded to .md. This means:

  • The attacker cannot control the file extension (no .sh, .py, .bashrc, etc.)
  • The attacker cannot inject special characters into the filename
  • The attacker can control the directory path and the file content
  • Null byte injection (\x00) does not work in Node.js fs module — it throws ERR_INVALID_ARG_VALUE

While the .md extension constraint significantly limits exploitability (no script execution, no crontab abuse, no SSH key injection), the path traversal itself is a real security defect that violates the principle of least privilege.

Additionally, this vulnerability can be chained with the set_project tool: by first using create_doc to create a .knowns/ directory structure at an arbitrary location, an attacker can then call set_project to hijack the project root to that location, further amplifying the attack surface for all subsequent MCP operations.

Vulnerable Code (snippet)

File: src/mcp/handlers/doc.ts, lines 494-504

if (input.folder) {
    const folderPath = input.folder.replace(/^\/|\/$/g, "");
    targetDir = join(getDocsDir(), folderPath);
    relativePath = join(folderPath, filename);

    if (!existsSync(targetDir)) {
        await mkdir(targetDir, { recursive: true });
    }
}

const filepath = join(targetDir, filename);
// ...
await writeFile(filepath, fileContent, "utf-8");

Impact

  • .md file creation at any writable filesystem location (extension is hardcoded — NOT arbitrary file write)
  • Arbitrary directory creation via mkdir(targetDir, { recursive: true })
  • File content is fully attacker-controlled (via the content parameter), but only written as .md
  • Can be chained with set_project to hijack project root (by creating .knowns/ at an attacker-chosen path, then calling set_project to switch to it)
  • Cannot overwrite existing files (the existsSync check prevents this — only writes if the target file does not already exist)

What this vulnerability CANNOT do

  • Write executable scripts (.sh, .py, .js, etc.) — extension is hardcoded to .md
  • Overwrite ~/.bashrc, ~/.ssh/authorized_keys, crontab entries, etc.
  • Achieve direct code execution — crontab and shell interpreters do not execute .md files
  • Read arbitrary files (this is a write-only vulnerability)

Steps to Reproduce

  1. Start the knowns MCP server: npx tsx --import ./scripts/md-loader.mjs src/mcp/server.ts
  2. Connect an MCP client (e.g., MCP Inspector) to the server via stdio.
  3. Call get_current_project to confirm the current project root.
  4. Call create_doc with a folder parameter containing path traversal sequences.

Proof of Concept

Goal: demonstrate arbitrary file write outside the .knowns/docs/ directory without destructive operations.

PoC 1: Escape .knowns/docs/ — write to project root

{
  "title": "traversal-level1",
  "folder": "../../tmp/knowns-poc-traversal",
  "content": "# PoC Level 1\nEscaped .knowns/docs/ — written to ~/tmp/"
}

Expected result:

  • File traversal-level1.md is created at <projectRoot>/tmp/knowns-poc-traversal/traversal-level1.md, which is outside .knowns/docs/.

PoC 2: Escape project root entirely — write to /tmp/

{
  "title": "traversal-level2",
  "folder": "../../../../tmp/knowns-poc-traversal",
  "content": "# PoC Level 2\nEscaped home directory — written to /tmp/"
}

Expected result:

  • File traversal-level2.md is created at /tmp/knowns-poc-traversal/traversal-level2.md, completely outside the project directory.

PoC 3: Chain with set_project for project root hijacking

// Step 1: Create .knowns structure at attacker-chosen location
// Tool: create_doc
{
  "title": "bootstrap",
  "folder": "../../../../tmp/knowns-hijack/.knowns/docs",
  "content": "bootstrap"
}

// Step 2: Hijack project root
// Tool: set_project
{ "projectRoot": "/tmp/knowns-hijack" }

// Step 3: Verify hijack
// Tool: get_current_project
{}
// Returns: { "projectRoot": "/tmp/knowns-hijack", "isValid": true }

Expected result:

  • set_project succeeds because .knowns/ now exists at /tmp/knowns-hijack/.knowns/
  • All subsequent MCP operations use /tmp/knowns-hijack as the project root

Tested result

  • All three PoCs confirmed successful on macOS with knowns v0.11.4 (commit 62fb0da)

Recommended Remediation

  1. Validate folder against path traversal by resolving the full path and verifying it remains under the intended base directory:
    import { resolve } from "node:path";
    
    const resolvedDir = resolve(getDocsDir(), folderPath);
    if (!resolvedDir.startsWith(resolve(getDocsDir()))) {
        throw new Error("Path traversal detected in folder parameter");
    }
    targetDir = resolvedDir;
  2. Additionally reject path components containing .. as a defense-in-depth measure.
  3. Apply the same path validation to resolveDocPath() (used by get_doc and update_doc) which has a similar vulnerability with the path parameter.
  4. Add regression tests for path traversal payloads in folder and path parameters.

References

@spdc-elm

Copy link
Copy Markdown
Author
image image image image image image image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment