GHSA-X227-PF99-VFFG
Vulnerability from github – Published: 2026-06-18 13:55 – Updated: 2026-06-18 13:55The MCP SSE server started via ToolsMCPServer.run_sse() / launch_tools_mcp_server(transport="sse") binds to 0.0.0.0 by default and builds its Starlette application with no authentication middleware and no Origin-header validation. The module mcp/mcp_security.py provides exactly the needed controls (origin validation, DNS-rebinding detection, auth-header enforcement, a SecurityConfig), but none of these functions are ever called by any transport — they are dead code. Any host that can reach the port can list and invoke every registered tool with no credentials, and a victim's browser can drive the same calls against a localhost instance via DNS rebinding.
Affected code: src/praisonai-agents/praisonaiagents/mcp/mcp_server.py
- run_sse defaults host to all interfaces (line 245) and builds the app with only debug and routes
- no middleware= and no per-route auth/origin gate (lines ~271-289):
app = Starlette(debug=self._debug, routes=[
Route(sse_path, endpoint=handle_sse), # "/sse"
Mount(messages_path, app=sse_transport.handle_post_message), # "/messages/"
])
uvicorn.run(app, host=host, port=port)
- launch_tools_mcp_server also defaults host="0.0.0.0" (line 301).
src/praisonai-agents/praisonaiagents/mcp/mcp_security.py defines but the transports never call: - is_valid_origin (line 30), is_potential_dns_rebinding (line 110), validate_auth_header (line 167), SecurityConfig.is_origin_allowed (line 236). These symbols are referenced only inside mcp_security.py and the init re-export. (mcp_websocket.py's auth references are CLIENT-side, not server validation.)
Impact: launch_tools_mcp_server(transport="sse") is the documented path for exposing tools over MCP. With the defaults above it is an unauthenticated, network-reachable tool-execution endpoint. Blast radius equals the capabilities of the registered tools; with file/shell/code-exec tools this is RCE. With no Origin check, a malicious page the victim merely visits can rebind its hostname to 127.0.0.1 and issue the JSON-RPC calls cross-origin against a developer's local server.
Proof of concept: Static proof (AST analysis of unmodified source): Check 1 - run_sse(host='0.0.0.0'); launch_tools_mcp_server(host='0.0.0.0') -> EXPOSED Check 2 - Starlette(...) kwargs: ['debug','routes'] -> NO middleware= (no auth/origin gate) Check 3 - is_valid_origin / is_potential_dns_rebinding / validate_auth_header / SecurityConfig never called by any transport -> DEAD CODE Live exploitation against a running server: curl -N http://VICTIM:8080/sse # event: endpoint / data: /messages/?session_id= curl -X POST "http://VICTIM:8080/messages/?session_id=" -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05", "capabilities":{},"clientInfo":{"name":"x","version":"1"}}}' curl -X POST "http://VICTIM:8080/messages/?session_id=" -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"","arguments":{...}}}' No Authorization header anywhere. Browser DNS-rebinding variant drives the same calls cross-origin.
Remediation: Wire in the existing mcp_security.py controls and fix defaults: - Default run_sse(host="127.0.0.1"); require explicit opt-in to bind 0.0.0.0. - Attach Starlette middleware calling is_valid_origin / is_potential_dns_rebinding; reject bad origins. - Enforce validate_auth_header when SecurityConfig.require_auth; default require_auth=True (and allow_missing_origin=False) for any non-loopback bind.
Distinct from prior advisories: The accepted MCP advisories are tool-handler bugs — tools/call path traversal -> .pth RCE (GHSA-9mqq-jqxf-grvw) and unauthenticated file read via workflow.show/validate (GHSA-9cr9-25q5-8prj). This is a transport-layer missing-auth/exposure: the SSE server never enforces auth or Origin validation and ignores the security module the codebase ships. Closest in spirit to the default-insecure pattern (GHSA-8444 / 86qc) but a different server and a different root cause (unwired controls, not an unset env var).
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "praisonaiagents"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.6.59"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-1327",
"CWE-306",
"CWE-350"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-18T13:55:54Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "The MCP SSE server started via ToolsMCPServer.run_sse() / launch_tools_mcp_server(transport=\"sse\")\nbinds to 0.0.0.0 by default and builds its Starlette application with no authentication middleware\nand no Origin-header validation. The module mcp/mcp_security.py provides exactly the needed controls\n(origin validation, DNS-rebinding detection, auth-header enforcement, a SecurityConfig), but none of\nthese functions are ever called by any transport \u2014 they are dead code. Any host that can reach the\nport can list and invoke every registered tool with no credentials, and a victim\u0027s browser can drive\nthe same calls against a localhost instance via DNS rebinding.\n\nAffected code: src/praisonai-agents/praisonaiagents/mcp/mcp_server.py\n- run_sse defaults host to all interfaces (line 245) and builds the app with only `debug` and `routes`\n - no `middleware=` and no per-route auth/origin gate (lines ~271-289):\n app = Starlette(debug=self._debug, routes=[\n Route(sse_path, endpoint=handle_sse), # \"/sse\"\n Mount(messages_path, app=sse_transport.handle_post_message), # \"/messages/\"\n ])\n uvicorn.run(app, host=host, port=port)\n- launch_tools_mcp_server also defaults host=\"0.0.0.0\" (line 301).\n\nsrc/praisonai-agents/praisonaiagents/mcp/mcp_security.py defines but the transports never call:\n- is_valid_origin (line 30), is_potential_dns_rebinding (line 110), validate_auth_header (line 167),\n SecurityConfig.is_origin_allowed (line 236). These symbols are referenced only inside mcp_security.py\n and the __init__ re-export. (mcp_websocket.py\u0027s auth references are CLIENT-side, not server validation.)\n\nImpact:\nlaunch_tools_mcp_server(transport=\"sse\") is the documented path for exposing tools over MCP. With the\ndefaults above it is an unauthenticated, network-reachable tool-execution endpoint. Blast radius equals\nthe capabilities of the registered tools; with file/shell/code-exec tools this is RCE. With no Origin\ncheck, a malicious page the victim merely visits can rebind its hostname to 127.0.0.1 and issue the\nJSON-RPC calls cross-origin against a developer\u0027s local server.\n\nProof of concept:\nStatic proof (AST analysis of unmodified source):\n Check 1 - run_sse(host=\u00270.0.0.0\u0027); launch_tools_mcp_server(host=\u00270.0.0.0\u0027) -\u003e EXPOSED\n Check 2 - Starlette(...) kwargs: [\u0027debug\u0027,\u0027routes\u0027] -\u003e NO middleware= (no auth/origin gate)\n Check 3 - is_valid_origin / is_potential_dns_rebinding / validate_auth_header / SecurityConfig\n never called by any transport -\u003e DEAD CODE\nLive exploitation against a running server:\n curl -N http://VICTIM:8080/sse\n # event: endpoint / data: /messages/?session_id=\u003csid\u003e\n curl -X POST \"http://VICTIM:8080/messages/?session_id=\u003csid\u003e\" -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\n \"capabilities\":{},\"clientInfo\":{\"name\":\"x\",\"version\":\"1\"}}}\u0027\n curl -X POST \"http://VICTIM:8080/messages/?session_id=\u003csid\u003e\" -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"\u003ctool\u003e\",\"arguments\":{...}}}\u0027\nNo Authorization header anywhere. Browser DNS-rebinding variant drives the same calls cross-origin.\n\nRemediation:\nWire in the existing mcp_security.py controls and fix defaults:\n- Default run_sse(host=\"127.0.0.1\"); require explicit opt-in to bind 0.0.0.0.\n- Attach Starlette middleware calling is_valid_origin / is_potential_dns_rebinding; reject bad origins.\n- Enforce validate_auth_header when SecurityConfig.require_auth; default require_auth=True (and\n allow_missing_origin=False) for any non-loopback bind.\n\nDistinct from prior advisories:\nThe accepted MCP advisories are tool-handler bugs \u2014 tools/call path traversal -\u003e .pth RCE\n(GHSA-9mqq-jqxf-grvw) and unauthenticated file read via workflow.show/validate (GHSA-9cr9-25q5-8prj).\nThis is a transport-layer missing-auth/exposure: the SSE server never enforces auth or Origin validation\nand ignores the security module the codebase ships. Closest in spirit to the default-insecure pattern\n(GHSA-8444 / 86qc) but a different server and a different root cause (unwired controls, not an unset env var).",
"id": "GHSA-x227-pf99-vffg",
"modified": "2026-06-18T13:55:54Z",
"published": "2026-06-18T13:55:54Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-x227-pf99-vffg"
},
{
"type": "PACKAGE",
"url": "https://github.com/MervinPraison/PraisonAI"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "PraisonAI: MCP SSE transport binds 0.0.0.0 with no authentication and no Origin validation; bundled SecurityConfig is never wired in"
}
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.