MCP---Agent-Starter-Kit Arbitrary File Read via MCP read_doc
1) CNA / Submission Type
- Submission type: Report a vulnerability (CVE ID request)
- Reporter role: Independent security researcher
- Report date: April 13, 2026
2) Reporter Contact
- Reporter name:
Winegee
- Reporter email:
Winegee@zju.edu.cn
- Permission to share contact with vendor:
Yes
3) Vendor / Product Identification
- Vendor: fub05
- Product: MCP---Agent-Starter-Kit
- Repository: https://github.com/fub05/MCP---Agent-Starter-Kit.git
- Reviewed local source path:
python-batch-04/datasets/fub05-MCP---Agent-Starter-Kit
- Exposed interface: MCP tool
read_doc (and parallel HTTP helper logic in /tool/get/{name})
- Affected component(s):
mcp-docs-server/app/mcp_adapter.py
mcp-docs-server/app/server.py
4) Vulnerability Type
- CWE: CWE-73 (External Control of File Name or Path)
- Short title: the MCP docs adapter reads arbitrary local files because
name is joined directly with the docs base path
5) Affected Versions
- Confirmed affected: current scanned revision in the local dataset
- Suspected affected range: revisions containing the same
self.base_docs / name logic
- Fixed version: Not available at time of report (April 12, 2026)
6) Vulnerability Description
The starter kit includes mcp-docs-server, which is supposed to expose only files from its local docs/ directory. In the optional MCP integration, the read_doc tool receives a caller-controlled name field and resolves it as self.base_docs / name without canonicalization or root-boundary checks.
For absolute paths, Path(base_docs) / "/etc/passwd" resolves to /etc/passwd directly. For relative traversal strings, the resulting path can also escape the docs directory. The HTTP helper route in server.py uses the same unchecked pattern, but the clearest externally reachable primitive is the MCP read_doc tool because it accepts free-form strings instead of a single path segment.
7) Technical Root Cause
- MCP registration
mcp-docs-server/app/mcp_adapter.py:25-33
read_doc reads name = params.get("name") and resolves p = (self.base_docs / name).
- Unchecked file read
mcp-docs-server/app/mcp_adapter.py:31-33
- If the resolved path exists, the server returns
p.read_text(encoding="utf-8").
- Parallel unchecked HTTP path construction
mcp-docs-server/app/server.py:27-32
target = BASE / name followed by FileResponse(str(target)).
- Audit conclusion
- The advertised docs-only trust boundary is broken for MCP callers because
name is treated as a filesystem path rather than a document identifier.
8) Attack Prerequisites
fastmcp installed and the MCP integration enabled.
- Ability to invoke the
read_doc MCP tool.
- Read permissions for the target local file under the server's OS account.
9) Proof of Concept / Reproduction Guidance
- Reproduction request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "read_doc",
"arguments": {
"name": "/etc/passwd"
}
}
}
- Why this works
- In
pathlib, self.base_docs / "/etc/passwd" resolves to /etc/passwd.
- The tool checks only
exists() and then returns the file contents.
- Expected observable result
- The tool responds with the contents of
/etc/passwd (or another attacker-selected readable file), not a document from the local docs/ directory.
- This PoC avoids HTTP path-segment encoding edge cases by targeting the MCP tool directly, which accepts a free-form string.
10) Security Impact
- Confidentiality: High, because arbitrary local files can be disclosed to the MCP caller.
- Integrity: Low, because the confirmed primitive is read-only.
- Availability: Low, aside from incidental load or disclosure of large files.
- Scope: Unchanged.
11) CVSS v3.1 Suggestion
- Suggested vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:L
- Suggested base score:
8.6 (High) when the MCP server is exposed to untrusted clients.
12) Workarounds / Mitigations
- Disable the MCP docs adapter until path validation is added.
- Run the server only in trusted local environments.
- Restrict the docs server process to a tightly sandboxed filesystem view if possible.
13) Recommended Fix
- Normalize and resolve the requested path, then reject anything outside
base_docs.
- Prefer document IDs or
basename-only selection instead of raw path strings.
- Add regression tests for absolute paths and
../ traversal through the MCP interface.
14) References
15) Credits
- Discoverer:
Winegee
- Discovery method: Static analysis (CodeQL) plus local source-code audit
16) Additional Notes
- The README states the service should expose a local
docs folder as a retrieval tool. The confirmed issue is specifically that this boundary is not enforced for MCP callers.
- The HTTP helper path was not needed for confirmation because the MCP adapter alone provides a complete arbitrary-read primitive.
MCP---Agent-Starter-Kit Arbitrary File Read via MCP
read_doc1) CNA / Submission Type
2) Reporter Contact
WinegeeWinegee@zju.edu.cnYes3) Vendor / Product Identification
python-batch-04/datasets/fub05-MCP---Agent-Starter-Kitread_doc(and parallel HTTP helper logic in/tool/get/{name})mcp-docs-server/app/mcp_adapter.pymcp-docs-server/app/server.py4) Vulnerability Type
nameis joined directly with the docs base path5) Affected Versions
self.base_docs / namelogic6) Vulnerability Description
The starter kit includes
mcp-docs-server, which is supposed to expose only files from its localdocs/directory. In the optional MCP integration, theread_doctool receives a caller-controllednamefield and resolves it asself.base_docs / namewithout canonicalization or root-boundary checks.For absolute paths,
Path(base_docs) / "/etc/passwd"resolves to/etc/passwddirectly. For relative traversal strings, the resulting path can also escape the docs directory. The HTTP helper route inserver.pyuses the same unchecked pattern, but the clearest externally reachable primitive is the MCPread_doctool because it accepts free-form strings instead of a single path segment.7) Technical Root Cause
mcp-docs-server/app/mcp_adapter.py:25-33read_docreadsname = params.get("name")and resolvesp = (self.base_docs / name).mcp-docs-server/app/mcp_adapter.py:31-33p.read_text(encoding="utf-8").mcp-docs-server/app/server.py:27-32target = BASE / namefollowed byFileResponse(str(target)).nameis treated as a filesystem path rather than a document identifier.8) Attack Prerequisites
fastmcpinstalled and the MCP integration enabled.read_docMCP tool.9) Proof of Concept / Reproduction Guidance
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "read_doc", "arguments": { "name": "/etc/passwd" } } }pathlib,self.base_docs / "/etc/passwd"resolves to/etc/passwd.exists()and then returns the file contents./etc/passwd(or another attacker-selected readable file), not a document from the localdocs/directory.10) Security Impact
11) CVSS v3.1 Suggestion
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:L8.6 (High)when the MCP server is exposed to untrusted clients.12) Workarounds / Mitigations
13) Recommended Fix
base_docs.basename-only selection instead of raw path strings.../traversal through the MCP interface.14) References
mcp-docs-server/app/mcp_adapter.py,mcp-docs-server/app/server.py15) Credits
Winegee16) Additional Notes
docsfolder as a retrieval tool. The confirmed issue is specifically that this boundary is not enforced for MCP callers.