NewApp Monitoring is liveSign up free
AgentbeamBeta
AI agent vulnerability · Other MCP servers and tools

CVE-2026-49257: mcp-pinot: Unauthenticated tool invocation via default oauth_enabled=False + host 0.0.0.0 bind

criticalCVSS 3.1 10.0GHSA-73cv-556c-w3g6

mcp-pinot (PyPI package mcp-pinot-server) up to 3.0.1 runs its HTTP MCP server on 0.0.0.0:8080 with OAuth off by default. Anyone who can reach the port can call all 14 tools, including SQL queries and schema changes, using the server's own Pinot credentials. Upgrade to 3.1.0, which binds to loopback and requires OAuth for network exposure.

Published Jun 26, 2026 · Analysis Oct 25, 2026

What an attacker can do

Three defaults combine. oauth_enabled is False in mcp_pinot/config.py, so server.py never builds the OAuth proxy and FastMCP registers every @mcp.tool with no authentication. The default host is 0.0.0.0 and port is 8080. Following the README's HTTP setup gives a network-reachable server with no auth.

The server calls Pinot with credentials from its environment (PINOT_TOKEN, or PINOT_USERNAME and PINOT_PASSWORD), usually a privileged service account. An unauthenticated caller borrows that identity, a confused-deputy case. A single POST to /mcp with tools/call is enough.

The exposed tools let the caller:

  • run arbitrary SELECT queries with read_query and enumerate tables with list_tables
  • read segment, index and table-config metadata
  • create or change schemas and table configs with create_schema, update_schema, create_table_config and update_table_config
  • see connection details through test_connection, and filter lists through reload_table_filters

Writes work wherever the service account has write rights. Heavy queries or bad configs can also disrupt the cluster. The advisory scores it 10.0 with a changed scope, because the flaw reaches a separate Pinot cluster.

Who is affected

mcp-pinot-server 3.0.1 and earlier. The advisory confirms tags v2.1.0 through v3.0.1. Deployments using the HTTP transport are exposed. Setting MCP_HOST=127.0.0.1 limits reach to the local machine, and the stdio transport does not listen on the network.

pip show mcp-pinot-server

Also check which interface the process listens on, for example with ss -ltnp and a look for port 8080.

mcp-pinot-server

Ecosystem
PyPI
Affected
<= 3.0.1
Fixed in
3.1.0
Check
pip show mcp-pinot-server

How to fix it

Upgrade to mcp-pinot-server 3.1.0, released 2026-05-25.

pip install --upgrade mcp-pinot-server==3.1.0

Pull request 95 changes the default MCP_HOST to 127.0.0.1. The server now refuses to start with HTTP bound to a non-loopback host unless OAUTH_ENABLED=true. Helm exposure is opt-in and needs OAuth. The read-query tool now parses SQL with sqlglot and accepts only one SELECT or WITH ... SELECT statement.

If you must reach the server over a network, enable OAuth. Until you upgrade, set MCP_HOST=127.0.0.1 and firewall port 8080.

How to tell if you were hit

Check the access logs of the host or reverse proxy for unauthenticated POST requests to /mcp from unexpected addresses. Look in the Pinot controller and broker logs for queries, schema creations or table-config updates made by the MCP service account at odd times, such as a schema named attacker_schema, the name used in the advisory's reproduction. Rotate the Pinot credentials the server used if it was reachable from untrusted networks.

Original advisory

From the GitHub Advisory Database (CC BY 4.0).

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:

| 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):

@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):

_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):

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:

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.

Credited to raysabee, PeledTomer1.

Questions

Is mcp-pinot 3.0.1 affected by CVE-2026-49257?

Yes. Versions up to and including 3.0.1 are affected. The fix is in 3.1.0.

Am I safe if I run mcp-pinot over stdio?

The flaw concerns the HTTP transport bound to 0.0.0.0:8080 without auth. The advisory's attack needs a network-reachable HTTP server. Binding to 127.0.0.1 also reduces the attack vector to local.

Will 3.1.0 break my deployment?

It can. A non-loopback HTTP bind without OAuth now fails to start. Helm installs no longer create a Service by default. Multi-statement or non-SELECT queries in read-query are rejected.

References

How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology

More Other MCP servers and tools vulnerabilities

All Other MCP servers and tools entries