PT-2026-53492 · Pypi · Mcp-Pinot-Server

Published

2026-06-29

·

Updated

2026-06-29

CVSS v3.1

10

Critical

VectorAV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

Resolution

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:
MetricValueReason
AVNetworkThe default HTTP server bound to 0.0.0.0:8080 and accepted remote HTTP requests.
ACLowExploitation required only a direct MCP tool call.
PRNoneOAuth was disabled by default.
UINoneNo user interaction was required.
SChangedThe vulnerable MCP server used its server-side credentials to act on the separate Pinot cluster security boundary.
CHighUnauthenticated callers could read table data and cluster metadata through server-side Pinot credentials.
IHighUnauthenticated callers could create or update schemas and table configs where the server-side account had those privileges.
AHighExpensive 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:
ToolImpact when unauthenticated
read queryArbitrary SELECT against any table allowed by server-side filter (or all tables if no filter)
list tablesEnumerate cluster schemas
table details, segment list, segment metadata details, tableconfig schema details, index column details, get schema, get table configRead cluster metadata
create schema, update schemaCreate or mutate Pinot schemas
create table config, update table configCreate or mutate table configurations
reload table filtersReload server filter file; response leaks previous filters and new filters lists
test connectionCluster 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:
bash
# 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
MetricValueReason
AV (Attack Vector)NetworkServer defaults to bind on 0.0.0.0:8080
AC (Attack Complexity)LowNo special conditions, single HTTP request
PR (Privileges Required)NoneNo authentication required in default config
UI (User Interaction)NoneDirect unauthenticated call
S (Scope)ChangedVulnerable MCP component grants access to a separate Pinot cluster (different security authority)
C (Confidentiality)HighFull read of any table data the server-side account can reach
I (Integrity)HighSchema and table-config writes via create schema, update schema, create table config, update table config
A (Availability)HighHeavy 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.py lines 26-46, 248-269
  • mcp pinot/config.py lines 56-65, 318-330
  • FastMCP auth parameter behavior when None: 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.

Fix

Found an issue in the description? Have something to add? Feel free to write us 👾

Related Identifiers

PYSEC-2026-410

Affected Products

Mcp-Pinot-Server