Skip to content

Commit c3fa6ac

Browse files
1 parent e3d5cc4 commit c3fa6ac

3 files changed

Lines changed: 218 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-2r68-g678-7qr3",
4+
"modified": "2026-06-26T21:04:38Z",
5+
"published": "2026-06-26T21:04:38Z",
6+
"aliases": [
7+
"CVE-2026-49291"
8+
],
9+
"summary": "mcp-memory-service: OAuth read-only clients can write and delete memories through MCP tools/call",
10+
"details": "## Summary\n\nThe HTTP MCP JSON-RPC endpoint at `/mcp` requires only OAuth `read` scope for all requests, then dispatches `tools/call` directly to handlers that include mutating tools. A read-only OAuth client can call `store_memory` and `delete_memory` through MCP even though the corresponding REST endpoints require `write` scope.\n\n## Technical Details\n\n`src/mcp_memory_service/web/api/mcp.py` declares `mcp_endpoint` with `user: AuthenticationResult = Depends(require_read_access)`. For `tools/call`, it extracts the requested tool name and arguments, then calls `handle_tool_call(storage, tool_name, arguments)` without passing the authenticated user or checking a per-tool required scope.\n\nThe MCP tool registry includes both read tools and write tools. In the same handler file, `store_memory` creates a `Memory` object and calls `storage.store(...)`, while `delete_memory` calls `storage.delete(content_hash)`. These operations are reachable with only the `read` scope.\n\nThe REST endpoint demonstrates the intended boundary: `POST /api/memories` uses `Depends(require_write_access)` and rejects a read-only token with 403 `insufficient_scope`.\n\n## Reproduction\n\n1. Enable OAuth and disable anonymous access.\n2. Generate a valid OAuth JWT with only `scope: read`.\n3. Confirm the REST write endpoint rejects it:\n\n```http\nPOST /api/memories\nAuthorization: Bearer <read-only-token>\nContent-Type: application/json\n\n{\"content\":\"rest denied control\"}\n```\n\nExpected and observed: HTTP 403 with `Required scope 'write' not granted`.\n\n4. Send the same read-only token to the MCP endpoint:\n\n```http\nPOST /mcp\nAuthorization: Bearer <read-only-token>\nContent-Type: application/json\n\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"store_memory\",\n \"arguments\": {\n \"content\": \"mcp read scope stored this\",\n \"tags\": [\"poc\"]\n }\n }\n}\n```\n\nObserved: HTTP 200 JSON-RPC success and the storage `store` sink is reached.\n\n5. A read-only token can also call `delete_memory` through MCP if it knows a content hash:\n\n```http\nPOST /mcp\nAuthorization: Bearer <read-only-token>\nContent-Type: application/json\n\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 2,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"delete_memory\",\n \"arguments\": {\"content_hash\": \"<known_hash>\"}\n }\n}\n```\n\nObserved: HTTP 200 JSON-RPC success and the storage `delete` sink is reached.\n\n## Impact\n\nA client intended to be read-only can inject or delete memories through the MCP API. This can corrupt the memory database, influence future agent context, and destroy stored user memories without the OAuth `write` scope required by the REST API.\n\n## Affected Versions\n\nConfirmed present on current main commit `c99a922477df41f75a44db11182ae48a57311910` and latest release tag `v10.65.0` (`4eb4a62665589f9dd9f8c393afa32de434b4098a`).\n\n## Suggested Fix\n\nEnforce authorization per MCP tool at `tools/call` time. Require `write` for `store_memory` and `delete_memory`, keep `read` only for read-only tools, and add regression tests proving direct `tools/call` to mutating tools is rejected before the handler reaches storage when the caller has only `read` scope.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "mcp-memory-service"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "10.65.3"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 10.65.1"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/doobidoo/mcp-memory-service/security/advisories/GHSA-2r68-g678-7qr3"
45+
},
46+
{
47+
"type": "ADVISORY",
48+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-49291"
49+
},
50+
{
51+
"type": "PACKAGE",
52+
"url": "https://github.com/doobidoo/mcp-memory-service"
53+
},
54+
{
55+
"type": "WEB",
56+
"url": "https://pypi.org/project/mcp-memory-service/10.65.3"
57+
},
58+
{
59+
"type": "WEB",
60+
"url": "https://web.archive.org/web/20260508112116/https://github.com/doobidoo/mcp-memory-service"
61+
}
62+
],
63+
"database_specific": {
64+
"cwe_ids": [
65+
"CWE-862"
66+
],
67+
"severity": "HIGH",
68+
"github_reviewed": true,
69+
"github_reviewed_at": "2026-06-26T21:04:38Z",
70+
"nvd_published_at": "2026-06-19T19:16:36Z"
71+
}
72+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-73cv-556c-w3g6",
4+
"modified": "2026-06-26T21:05:43Z",
5+
"published": "2026-06-26T21:05:43Z",
6+
"aliases": [
7+
"CVE-2026-49257"
8+
],
9+
"summary": "mcp-pinot: Unauthenticated tool invocation via default oauth_enabled=False + host 0.0.0.0 bind",
10+
"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() -> 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\nhost: 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's 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'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.\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 'Content-Type: application/json' \\\n -d '{\n \"jsonrpc\":\"2.0\",\n \"method\":\"tools/call\",\n \"params\":{\"name\":\"list_tables\",\"arguments\":{}},\n \"id\":1\n }'\n\n# 2. Read arbitrary table contents (server forwards using its own Pinot credentials)\ncurl -X POST http://victim:8080/mcp \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"jsonrpc\":\"2.0\",\n \"method\":\"tools/call\",\n \"params\":{\n \"name\":\"read_query\",\n \"arguments\":{\"query\":\"SELECT * FROM <table> LIMIT 100\"}\n },\n \"id\":2\n }'\n\n# 3. Create a new schema (write privileges)\ncurl -X POST http://victim:8080/mcp \\\n -H 'Content-Type: application/json' \\\n -d '{\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 }'\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) -> 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.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "mcp-pinot-server"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "3.1.0"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 3.0.1"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/startreedata/mcp-pinot/security/advisories/GHSA-73cv-556c-w3g6"
45+
},
46+
{
47+
"type": "ADVISORY",
48+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-49257"
49+
},
50+
{
51+
"type": "WEB",
52+
"url": "https://github.com/startreedata/mcp-pinot/issues/90"
53+
},
54+
{
55+
"type": "WEB",
56+
"url": "https://github.com/startreedata/mcp-pinot/pull/95"
57+
},
58+
{
59+
"type": "WEB",
60+
"url": "https://github.com/startreedata/mcp-pinot/commit/1c7d3f9cd384854bf72c127d230bdb32299475ad"
61+
},
62+
{
63+
"type": "PACKAGE",
64+
"url": "https://github.com/startreedata/mcp-pinot"
65+
}
66+
],
67+
"database_specific": {
68+
"cwe_ids": [
69+
"CWE-306"
70+
],
71+
"severity": "CRITICAL",
72+
"github_reviewed": true,
73+
"github_reviewed_at": "2026-06-26T21:05:43Z",
74+
"nvd_published_at": "2026-06-18T21:16:29Z"
75+
}
76+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-jv46-xfwm-36j7",
4+
"modified": "2026-06-26T21:05:13Z",
5+
"published": "2026-06-26T21:05:13Z",
6+
"aliases": [
7+
"CVE-2026-49454"
8+
],
9+
"summary": "Relyra SAML SignatureValue not cryptographically verified -> authentication bypass",
10+
"details": "## Summary\n\nRelyra `1.0.0` and `1.1.0` accept forged SAML signatures because `SignatureValue` was not cryptographically verified before the library returned a successful authentication result.\n\n## Details\n\nIn `1.0.0` and `1.1.0`, the XMLDSig trust boundary was incomplete. `:public_key.verify` over the exclusive-C14N canonicalized `SignedInfo` was not performed against the configured IdP certificate's public key, `DigestValue` was not recomputed over the canonicalized referenced element, and `canonicalize/2` remained an unused passthrough in the signature-verification path. The result was a structure-only acceptance path where document shape and trust-source rejection could succeed without proving the signature bytes.\n\n## Impact\n\nA forged `SignatureValue` carrying an attacker-controlled `NameID` can be accepted as `{:ok}`. Any relying-party application using Relyra `1.0.0` or `1.1.0` can be logged into as an arbitrary user if it trusts the affected response path.\n\n## Patches\n\nRelyra `1.2.0` closes the gap with real exclusive-C14N canonicalization, `:public_key.verify` against the configured IdP certificate's public key, and a constant-time `DigestValue` recompute/compare bound to the exact consumed node on both `verify/4` and `verify_metadata_root/4`.\n\n## Workarounds\n\nThere is no safe configuration of `1.0.0` or `1.1.0`. Upgrade to `1.2.0` or later.\n\n## Resources\n\n- Fix commit `2e45689` (wire real XMLDSig crypto into the candidate arm)\n- Fix commit `8910200` (close metadata trust bypass, pin over DER)\n- Regression proof: `test/security/xml/adversarial_crypto_test.exs`, `test/relyra/metadata/auto_refresh_test.exs`, `test/security/ci_gate_integrity_test.exs`",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Hex",
21+
"name": "relyra"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "1.0.0"
29+
},
30+
{
31+
"fixed": "1.2.0"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/szTheory/relyra/security/advisories/GHSA-jv46-xfwm-36j7"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-49454"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/szTheory/relyra/commit/2e456897af3158c175bb490ce7fc51d6241c8922"
50+
},
51+
{
52+
"type": "WEB",
53+
"url": "https://github.com/szTheory/relyra/commit/8910200"
54+
},
55+
{
56+
"type": "PACKAGE",
57+
"url": "https://github.com/szTheory/relyra"
58+
}
59+
],
60+
"database_specific": {
61+
"cwe_ids": [
62+
"CWE-287",
63+
"CWE-347"
64+
],
65+
"severity": "CRITICAL",
66+
"github_reviewed": true,
67+
"github_reviewed_at": "2026-06-26T21:05:13Z",
68+
"nvd_published_at": "2026-06-18T21:16:29Z"
69+
}
70+
}

0 commit comments

Comments
 (0)