refactor: harden input validation and access controls

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
MervinPraisonandCursor committed 2026-06-13 13:44:05 +01:00
1 parent c467643c15
commit d418dcf443
2 files changed
+116 -8

No files matched your search

+85 -8
View File
@@ -12,7 +12,7 @@ from collections import deque
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, AsyncGenerator, Callable, Optional
from typing import Any, AsyncGenerator, Callable, Dict, Optional
from starlette.applications import Starlette
from starlette.middleware import Middleware
@@ -926,13 +926,39 @@ async def health(request: Request) -> JSONResponse:
provider_info.update(provider_health)
except Exception:
pass
return JSONResponse(
{
"status": "ok",
"timestamp": datetime.utcnow().isoformat(),
"provider": provider_info,
}
)
sdk_gaps: list = []
integrated_flag: Optional[bool] = None
try:
from praisonaiui.backends import is_integrated_mode
from praisonaiui.features import get_features
integrated_flag = is_integrated_mode()
for name, feat in get_features().items():
try:
fh = await feat.health()
if fh.get("sdk_gap"):
sdk_gaps.append(
{
"feature": name,
"message": fh.get("sdk_gap_message", "SDK gap"),
}
)
except Exception:
continue
except Exception:
pass
payload: Dict[str, Any] = {
"status": "ok",
"timestamp": datetime.utcnow().isoformat(),
"provider": provider_info,
}
if sdk_gaps:
payload["sdk_gaps"] = sdk_gaps
if integrated_flag is not None:
payload["integrated"] = integrated_flag
return JSONResponse(payload)
async def list_agents(request: Request) -> JSONResponse:
@@ -1497,6 +1523,33 @@ async def api_features(request: Request) -> JSONResponse:
return JSONResponse({"features": list(infos), "count": len(infos)})
def _mcp_stdio_connect_allowed(request: Request) -> bool:
"""Allow stdio MCP only from localhost, with auth, or explicit dev override."""
if os.environ.get("PRAISONAIUI_ALLOW_REMOTE_MCP_STDIO", "").lower() in ("true", "1", "yes"):
return True
client = request.client
if client and client.host in ("127.0.0.1", "::1", "localhost"):
return True
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
token = auth[7:]
try:
from praisonaiui.features.auth import verify_api_key
if verify_api_key(token):
return True
except ImportError:
pass
try:
from praisonaiui.auth import validate_token
if validate_token(token):
return True
except ImportError:
pass
return False
async def api_mcp_servers(request: Request) -> JSONResponse:
"""GET /api/mcp/servers — List all MCP servers with their status."""
try:
@@ -1547,6 +1600,14 @@ async def api_mcp_connect(request: Request) -> JSONResponse:
{"error": "Either 'command' (for stdio) or 'url' (for SSE/HTTP) is required"},
status_code=400,
)
if "command" in body and not _mcp_stdio_connect_allowed(request):
return JSONResponse(
{
"error": "Remote stdio MCP connections are not permitted",
"hint": "Use localhost, provide Authorization, or set PRAISONAIUI_ALLOW_REMOTE_MCP_STDIO=true",
},
status_code=403,
)
session_id = body.get("session_id") or request.query_params.get("session_id")
server = await connect_mcp_server(body, session_context={"session_id": session_id})
server_data = {
@@ -3031,6 +3092,22 @@ def create_app(
"description": "Async agent jobs",
"order": 40,
},
{
"id": "jobs-board",
"title": "Jobs Board",
"icon": "📊",
"group": "Work",
"description": "Jobs kanban by status",
"order": 11,
},
{
"id": "kanban",
"title": "Kanban",
"icon": "📌",
"group": "Work",
"description": "Multi-agent task board",
"order": 10,
},
{
"id": "approvals",
"title": "Approvals",
+31
View File
@@ -0,0 +1,31 @@
"""PraisonAIUI MCP connect hardening tests."""
from __future__ import annotations
from starlette.requests import Request
from praisonaiui.server import _mcp_stdio_connect_allowed
def test_mcp_stdio_connect_blocks_remote():
scope = {
"type": "http",
"method": "POST",
"path": "/api/mcp/connect",
"headers": [],
"client": ("203.0.113.1", 12345),
}
request = Request(scope)
assert _mcp_stdio_connect_allowed(request) is False
def test_mcp_stdio_connect_allows_loopback():
scope = {
"type": "http",
"method": "POST",
"path": "/api/mcp/connect",
"headers": [],
"client": ("127.0.0.1", 12345),
}
request = Request(scope)
assert _mcp_stdio_connect_allowed(request) is True