CVE-2026-77604: Code Graph RAG · MCP paginated read_file path traversal / arbitrary file read
Affects code-graph-ragSource GCVE Database
Not known to be exploited · CVSS 7.5 High : upgrade to 0.0.345 soon.
Data as of 9 Nov 2026, 23:24 UTC
Thin evidence
One scorer or none| CNA (GitHub_M) | CVSS 3.1 | 7.5 |
|---|
- EPSS
- 0.3% chance of exploitation in 30 days – higher than 32% of CVEsExploit Prediction Scoring System: the likelihood attackers will use this flaw soon.
- Patch
- Available – fixed in 0.0.345
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NExploitation evidence
Disclosed: evidence found.
CVE published 5 Nov 2026
Detectable: no evidence on record.
No public nuclei template indexed
Weaponized: no evidence on record.
No public exploit code indexed in Metasploit or Exploit-DB
Exploited in the wildExploited: no evidence on record.
Not in CISA KEV and no source reports exploitation
Observed by OffSeq MirageSeen by Mirage: no evidence on record.
No attack traffic recorded by OffSeq Mirage sensors
A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 9 Nov 2026.
What to do
Upgrade code-graph-rag to 0.0.345.
Affected software
| Product | Affected | Fixed in |
|---|---|---|
| PyPIcode-graph-rag | Affected
| Fixed in
|
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
Description
### Summary The `read_file` MCP tool in `codebase_rag/mcp/tools.py` contains a path traversal vulnerability in its paginated branch (when `offset` or `limit` is supplied). The handler constructs a file path using `Path(self.project_root) / file_path` without subsequently validating that the resolved path remains within the configured project root. Python's `pathlib` silently discards the left-hand component when the right-hand argument is an absolute path, allowing an attacker to read any UTF-8-readable file on the server by passing an absolute path such as `/etc/passwd`. ### Details **Vulnerable location:** `codebase_rag/mcp/tools.py:556–565` (`MCPToolsRegistry.read_file`) The `read_file` MCP tool accepts three parameters: `file_path`, `offset`, and `limit`. When either `offset` or `limit` is present the handler takes a distinct code path: ```python # codebase_rag/mcp/tools.py:556-565 async def read_file( self, file_path: str, offset: int | None = None, limit: int | None = None ) -> str: logger.info(lg.MCP_READ_FILE.format(path=file_path, offset=offset, limit=limit)) try: if offset is not None or limit is not None: full_path = Path(self.project_root) / file_path # ← VULNERABLE LINE start = offset if offset is not None else 0 with open(full_path, encoding=cs.ENCODING_UTF8) as f: # ← SINK ``` Because `pathlib.Path.__truediv__` replaces the left operand entirely when the right operand is an absolute path, any caller-supplied absolute path (e.g., `/etc/passwd`, `/proc/self/environ`) causes `full_path` to point outside the project root. The **non-paginated** branch is protected by the `@validate_project_path` decorator (applied via `codebase_rag/tools/file_reader.py:27`) which performs `.resolve().relative_to(project_root)` (`codebase_rag/decorators.py:73–75`), but this decorator is never invoked in the paginated branch, leaving it completely unguarded. **Full data-flow (source → sink):** 1. `codebase_rag/config.py:302` — HTTP MCP default host is `0.0.0.0`. 2. `codebase_rag/cli.py:717` — CLI exposes `mcp-server`. 3. `codebase_rag/cli.py:726–731` — `--transport http` invokes `serve_http(host=resolved_host, port=resolved_port)`. 4. `codebase_rag/mcp/server.py:208–210` — Starlette mounts the MCP HTTP endpoint. 5. `codebase_rag/mcp/tools.py:227–249` — `read_file` is registered as an MCP tool accepting attacker-controlled `file_path`, `offset`, and `limit`. 6. `codebase_rag/mcp/server.py:113–127` — `call_tool` dispatches JSON arguments directly to the handler via `await handler(**arguments)`. 7. `codebase_rag/mcp/tools.py:561–565` — paginated branch builds `full_path` and calls `open(full_path)` without any boundary check. The vulnerability is also present when using the `stdio` transport because the vulnerable code path does not depend on transport type; however, the attack is unauthenticated and network-accessible only when HTTP transport is configured. ### PoC **Prerequisites:** - Docker installed. - Repository cloned with the `vuln-001/` artifacts present (Dockerfile, poc.py). **Build and run:** ```bash # From the repository root (parent of vuln-001/) docker build -t vuln001-poc -f vuln-001/Dockerfile . docker run --rm vuln001-poc ``` The Dockerfile installs the `codebase_rag` package from the local repository source, creates a "safe" project root at `/tmp/safe-project-root`, and runs `poc.py`. **What the PoC does (`poc.py`):** 1. Pre-mocks the `mgclient` / `pymgclient` native C extensions (Memgraph client) so the package imports without a live database. 2. Instantiates `MCPToolsRegistry` via `object.__new__()`, setting only `self.project_root = "/tmp/safe-project-root"` — matching what the real server does. 3. Demonstrates the pathlib bypass: `Path("/tmp/safe-project-root") / "/etc/hosts"` resolves to `Path("/etc/hosts")`. 4. Calls `registry.read_file("/etc/hosts", offset=0, limit=5)` — the actual vulnerable method from the installed package. 5. Asserts that real `/etc/hosts` content is returned, confirming full boundary bypass. **Expected output:** ``` [MCP] read_file: /etc/hosts (offset=0, limit=5) [+] CONFIRMED: pathlib discards project_root when file_path is absolute [+] read_file() returned: # Lines 1-5 of 7 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00:: ip6-localnet ff00:: ip6-mcastprefix ff02::1 ip6-allnodes [+] EXPLOIT SUCCESSFUL [+] Content of '/etc/hosts' exfiltrated via path traversal [+] Project root boundary '/tmp/safe-project-root' was completely bypassed [+] VULNERABILITY CONFIRMED: VULN-001 CWE-22 Path Traversal ``` **Equivalent HTTP transport attack (when server is running):** ```bash python3 - <<'PY' import anyio from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client async def main(): async with streamablehttp_client("http://127.0.0.1:8080/mcp") as (read, write, _): async with ClientSession(read, write) as session: await session.i
References
- github.com/vitali87/code-graph-rag/security/advisories/GHSA-vvr2-h2jp-838m (opens in a new tab)
- nvd.nist.gov/vuln/detail/CVE-2026-77604 (opens in a new tab)
- github.com/vitali87/code-graph-rag/commit/6541e920a27961fa8f3ff49ef38c0e0bd09cd626 (opens in a new tab)
- github.com/vitali87/code-graph-rag (opens in a new tab)
- github.com/vitali87/code-graph-rag/releases/tag/v0.0.345 (opens in a new tab)