VYPR
Critical severity9.4NVD Advisory· Published Apr 21, 2026· Updated Apr 22, 2026

CVE-2026-40576

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.

PackageAffected versionsPatched versions
excel-mcp-serverPyPI
< 0.1.80.1.8

Affected products

1

Patches

1
f51340ecd577

Merge commit from fork

8 files changed · +649 570
  • excel-mcp-server-0.1.7.mcpb+0 0 removed
  • excel-mcp-server-0.1.8.mcpb+0 0 added
  • manifest.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

4

News mentions

0

No linked articles in our index yet.