CVE-2025-58358
Description
Markdownify is a Model Context Protocol server for converting almost anything to Markdown. Versions below 0.0.2 contain a command injection vulnerability, caused by the unsanitized use of input parameters within a call to child_process.exec, enabling an attacker to inject arbitrary system commands. Successful exploitation can lead to remote code execution under the server process's privileges. The server constructs and executes shell commands using unvalidated user input directly within command-line strings. This introduces the possibility of shell metacharacter injection (|, >, &&, etc.). This issue is fixed in version 0.0.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mcp-markdownify-servernpm | < 0.0.2 | 0.0.2 |
Affected products
1Patches
1a31204de058bMerge commit from fork
3 files changed · +41 −22
src/Markdownify.ts+24 −19 modified@@ -1,11 +1,12 @@ -import { exec } from "child_process"; +import { execFile } from "child_process"; import { promisify } from "util"; import path from "path"; import fs from "fs"; import os from "os"; import { fileURLToPath } from "url"; +import { expandHome } from "./utils.js"; -const execAsync = promisify(exec); +const execFileAsync = promisify(execFile); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -23,18 +24,24 @@ export class Markdownify { ): Promise<string> { const venvPath = path.join(projectRoot, ".venv"); const markitdownPath = path.join( - venvPath, - process.platform === 'win32' ? 'Scripts' : 'bin', - `markitdown${process.platform === 'win32' ? '.exe' : ''}` + venvPath, + process.platform === "win32" ? "Scripts" : "bin", + `markitdown${process.platform === "win32" ? ".exe" : ""}`, ); if (!fs.existsSync(markitdownPath)) { throw new Error("markitdown executable not found"); } - const { stdout, stderr } = await execAsync( - `${uvPath} run ${markitdownPath} "${filePath}"`, - ); + // Expand tilde in uvPath if present + const expandedUvPath = expandHome(uvPath); + + // Use execFile to prevent command injection + const { stdout, stderr } = await execFileAsync(expandedUvPath, [ + "run", + markitdownPath, + filePath, + ]); if (stderr) { throw new Error(`Error executing command: ${stderr}`); @@ -43,7 +50,10 @@ export class Markdownify { return stdout; } - private static async saveToTempFile(content: string | Buffer, suggestedExtension?: string | null): Promise<string> { + private static async saveToTempFile( + content: string | Buffer, + suggestedExtension?: string | null, + ): Promise<string> { let outputExtension = "md"; if (suggestedExtension != null) { outputExtension = suggestedExtension; @@ -60,13 +70,6 @@ export class Markdownify { private static normalizePath(p: string): string { return path.normalize(p); } - - private static expandHome(filepath: string): string { - if (filepath.startsWith('~/') || filepath === '~') { - return path.join(os.homedir(), filepath.slice(1)); - } - return filepath; - } static async toMarkdown({ filePath, @@ -126,14 +129,16 @@ export class Markdownify { filePath: string; }): Promise<MarkdownResult> { // Check file type is *.md or *.markdown - const normPath = this.normalizePath(path.resolve(this.expandHome(filePath))); + const normPath = this.normalizePath(path.resolve(expandHome(filePath))); const markdownExt = [".md", ".markdown"]; - if (!markdownExt.includes(path.extname(normPath))){ + if (!markdownExt.includes(path.extname(normPath))) { throw new Error("Required file is not a Markdown file."); } if (process.env?.MD_SHARE_DIR) { - const allowedShareDir = this.normalizePath(path.resolve(this.expandHome(process.env.MD_SHARE_DIR))); + const allowedShareDir = this.normalizePath( + path.resolve(expandHome(process.env.MD_SHARE_DIR)), + ); if (!normPath.startsWith(allowedShareDir)) { throw new Error(`Only files in ${allowedShareDir} are allowed.`); }
src/utils.ts+9 −0 added@@ -0,0 +1,9 @@ +import path from "path"; +import os from "os"; + +export function expandHome(filepath: string): string { + if (filepath.startsWith("~/") || filepath === "~") { + return path.join(os.homedir(), filepath.slice(1)); + } + return filepath; +}
src/UVX.ts+8 −3 modified@@ -1,6 +1,7 @@ -import { exec } from "child_process"; +import { execFile } from "child_process"; import { promisify } from "util"; -const execAsync = promisify(exec); +import { expandHome } from "./utils.js"; +const execFileAsync = promisify(execFile); export default class UVX { uvxPath: string; @@ -33,7 +34,11 @@ export default class UVX { async installDeps() { // This is a hack to make sure that markitdown is installed before it's called in the OCRProcessor try { - await execAsync(`${this.uvxPath} markitdown example.pdf`); + // Expand tilde in uvxPath if present + const expandedUvxPath = expandHome(this.uvxPath); + + // Use execFile to prevent command injection + await execFileAsync(expandedUvxPath, ["markitdown", "example.pdf"]); } catch { console.log("UVX markitdown should be ready now"); }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.