CVE-2026-40576
Description
excel-mcp-server is a Model Context Protocol server for Excel file manipulation. A path traversal vulnerability exists in excel-mcp-server versions up to and including 0.1.7. When running in SSE or Streamable-HTTP transport mode (the documented way to use this server remotely), an unauthenticated attacker on the network can read, write, and overwrite arbitrary files on the host filesystem by supplying crafted filepath arguments to any of the 25 exposed MCP tool handlers. The server is intended to confine file operations to a directory set by the EXCEL_FILES_PATH environment variable. The function responsible for enforcing this boundary — get_excel_path() — fails to do so due to two independent flaws: it passes absolute paths through without any check, and it joins relative paths without resolving or validating the result. Combined with zero authentication on the default network-facing transport and a default bind address of 0.0.0.0 (all interfaces), this allows trivial remote exploitation. This vulnerability is fixed in 0.1.8.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
excel-mcp-serverPyPI | < 0.1.8 | 0.1.8 |
Affected products
1- Range: <=0.1.7
Patches
1f51340ecd577Merge commit from fork
8 files changed · +649 −570
excel-mcp-server-0.1.7.mcpb+0 −0 removedexcel-mcp-server-0.1.8.mcpb+0 −0 addedmanifest.json+1 −1 modified@@ -1,7 +1,7 @@ { "manifest_version": "0.3", "name": "excel-mcp-server", - "version": "0.1.7", + "version": "0.1.8", "description": "A Model Context Protocol server for Excel file manipulation", "author": { "name": "haris",
pyproject.toml+1 −1 modified@@ -1,6 +1,6 @@ [project] name = "excel-mcp-server" -version = "0.1.7" +version = "0.1.8" description = "Excel MCP Server for manipulating Excel files" readme = "README.md" requires-python = ">=3.10"
README.md+1 −0 modified@@ -84,6 +84,7 @@ uvx excel-mcp-server streamable-http When running the server with the **SSE or Streamable HTTP protocols**, you **must set the `EXCEL_FILES_PATH` environment variable on the server side**. This variable tells the server where to read and write Excel files. - If not set, it defaults to `./excel_files`. +- With these transports, tool `filepath` values must be **relative** to that directory (e.g. `reports/q1.xlsx`); absolute paths and directory traversal are rejected. You can also set the `FASTMCP_PORT` environment variable to control the port the server listens on (default is `8017` if not set). - Example (Windows PowerShell):
src/excel_mcp/server.py+29 −10 modified@@ -72,26 +72,45 @@ instructions="Excel MCP Server for manipulating Excel files" ) + +def _resolved_path_is_within(base: str, candidate: str) -> bool: + base = os.path.realpath(base) + candidate = os.path.realpath(candidate) + if candidate == base: + return True + try: + return os.path.commonpath([base, candidate]) == base + except ValueError: + return False + + def get_excel_path(filename: str) -> str: """Get full path to Excel file. - + Args: filename: Name of Excel file - + Returns: Full path to Excel file """ - # If filename is already an absolute path, return it - if os.path.isabs(filename): - return filename + if not filename or "\x00" in filename: + raise ValueError(f"Invalid filename: {filename}") - # Check if in SSE mode (EXCEL_FILES_PATH is not None) if EXCEL_FILES_PATH is None: - # Must use absolute path - raise ValueError(f"Invalid filename: {filename}, must be an absolute path when not in SSE mode") + if not os.path.isabs(filename): + raise ValueError(f"Invalid filename: {filename}, must be an absolute path when not in SSE mode") + return os.path.normpath(filename) + + if os.path.isabs(filename): + raise ValueError(f"Invalid filename: {filename}, must be relative to EXCEL_FILES_PATH") + + base = os.path.realpath(EXCEL_FILES_PATH) + candidate = os.path.realpath(os.path.join(base, filename)) + + if not _resolved_path_is_within(base, candidate): + raise ValueError(f"Invalid filename: {filename}, path escapes EXCEL_FILES_PATH") - # In SSE mode, if it's a relative path, resolve it based on EXCEL_FILES_PATH - return os.path.join(EXCEL_FILES_PATH, filename) + return candidate @mcp.tool( annotations=ToolAnnotations(
tests/test_sandbox_paths.py+58 −0 added@@ -0,0 +1,58 @@ +import os +import sys +import tempfile +import unittest + +_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_SRC = os.path.join(_REPO_ROOT, "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +import excel_mcp.server as server # noqa: E402 + + +class TestGetExcelPathSandbox(unittest.TestCase): + def tearDown(self): + server.EXCEL_FILES_PATH = None + + def test_stdio_accepts_absolute_only(self): + server.EXCEL_FILES_PATH = None + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as f: + path = f.name + try: + self.assertEqual(server.get_excel_path(path), os.path.normpath(path)) + with self.assertRaises(ValueError): + server.get_excel_path("relative_only.xlsx") + finally: + os.unlink(path) + + def test_remote_rejects_absolute(self): + with tempfile.TemporaryDirectory() as d: + server.EXCEL_FILES_PATH = d + inner = os.path.join(d, "ok.xlsx") + with self.assertRaises(ValueError): + server.get_excel_path(inner) + + def test_remote_allows_relative_inside_sandbox(self): + with tempfile.TemporaryDirectory() as d: + server.EXCEL_FILES_PATH = d + out = server.get_excel_path(os.path.join("subdir", "file.xlsx")) + self.assertTrue(server._resolved_path_is_within(d, out)) + + def test_remote_blocks_traversal(self): + with tempfile.TemporaryDirectory() as d: + server.EXCEL_FILES_PATH = d + with self.assertRaises(ValueError): + server.get_excel_path("../outside.xlsx") + with self.assertRaises(ValueError): + server.get_excel_path(os.path.join("a", "..", "..", "outside.xlsx")) + + def test_remote_rejects_nul(self): + with tempfile.TemporaryDirectory() as d: + server.EXCEL_FILES_PATH = d + with self.assertRaises(ValueError): + server.get_excel_path("a\x00b.xlsx") + + +if __name__ == "__main__": + unittest.main()
uv.lock+559 −558 modified
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.