PraisonAI: MCP SSE transport binds 0.0.0.0 with no authentication and no Origin validation; bundled SecurityConfig is never wired in
The 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
debug and routes
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)src/praisonai-agents/praisonaiagents/mcp/mcp_security.py defines but the transports never call:
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
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:
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).
Why this VPI (explainable, experimental)
VPI breakdown
| Impact | 98.00 |
| Exploitation signal(No additional exploitation signal) | ×1.00 |
| VPI | 98.00 |
VPI formula vpi-v1