{"id":"CVE-2026-56838","aliases":["EUVD-2026-108879","GHSA-v847-hxxw-3pxg","PYSEC-2026-3520"],"status":"published","title":"PraisonAI recipe.run_stream skips dangerous-tool policy enforcement","description":"PraisonAI is a multi-agent teams system. From version 4.5.87 to before version 4.6.59, PraisonAI recipe execution blocks default-denied dangerous tools unless the caller explicitly passes allow_dangerous_tools=True. The normal recipe.run() path enforces this with _check_tool_policy(). The streaming path, recipe.run_stream(), loads the same recipe, checks dependencies, and then calls _execute_recipe() without running the dangerous-tool policy check. As a result, a recipe that honestly declares execute_command in TEMPLATE.yaml requires.tools is denied by recipe.run(), but reaches the execution engine through recipe.run_stream() with allow_dangerous_tools=False. This issue has been patched in version 4.6.59.","severity":{"version":"3.1","score":7.8,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00146,"percentile":0.03916,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-693","CWE-78","CWE-863"],"published":"2026-06-18T13:53:05Z","modified":"2026-10-31T02:30:55Z","fixes":[{"ecosystem":"PyPI","package":"praisonai","fixed_in":["4.6.59"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"praisonai","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.5.87"},{"last_affected":"4.6.58"},{"fixed":"4.6.59"}]}]},{"source":"osv","ecosystem":"PyPI","package":"praisonai","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.5.87"},{"fixed":"4.6.59"}]}],"versions":["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.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.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.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.140","4.5.143","4.5.144","4.5.145","4.5.149","4.5.87","4.5.88","4.5.89","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.58","4.6.9"]},{"source":"osv","ecosystem":"PyPI","package":"praisonai","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.5.87"},{"fixed":"4.6.59"}]}],"versions":["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.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.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.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.140","4.5.143","4.5.144","4.5.145","4.5.149","4.5.87","4.5.88","4.5.89","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.58","4.6.9"]}],"products":[{"vendor":"mervinpraison","product":"praisonai"}],"references":[{"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-v847-hxxw-3pxg","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/advisories/GHSA-v847-hxxw-3pxg","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/56xxx/CVE-2026-56838.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-56838","tags":["advisory"]},{"url":"https://github.com/MervinPraison/PraisonAI","tags":["package"]},{"url":"https://pypi.org/project/praisonai","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-56838","title":"PraisonAI recipe.run_stream skips dangerous-tool policy enforcement","description":"PraisonAI is a multi-agent teams system. From version 4.5.87 to before version 4.6.59, PraisonAI recipe execution blocks default-denied dangerous tools unless the caller explicitly passes allow_dangerous_tools=True. The normal recipe.run() path enforces this with _check_tool_policy(). The streaming path, recipe.run_stream(), loads the same recipe, checks dependencies, and then calls _execute_recipe() without running the dangerous-tool policy check. As a result, a recipe that honestly declares execute_command in TEMPLATE.yaml requires.tools is denied by recipe.run(), but reaches the execution engine through recipe.run_stream() with allow_dangerous_tools=False. This issue has been patched in version 4.6.59.","status":"published","published":"2026-10-23T11:58:01.475Z","modified":"2026-10-29T13:53:23.978Z","cvss":[{"version":"3.1","score":7.8,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high","assessor":"cna"}],"cwes":["CWE-78","CWE-693","CWE-863"],"updated_at":"2026-11-02T13:37:02.780526Z"}],"euvd":[{"id":"EUVD-2026-108879","description":"PraisonAI is a multi-agent teams system. From version 4.5.87 to before version 4.6.59, PraisonAI recipe execution blocks default-denied dangerous tools unless the caller explicitly passes allow_dangerous_tools=True. The normal recipe.run() path enforces this with _check_tool_policy(). The streaming path, recipe.run_stream(), loads the same recipe, checks dependencies, and then calls _execute_recipe() without running the dangerous-tool policy check. As a result, a recipe that honestly declares execute_command in TEMPLATE.yaml requires.tools is denied by recipe.run(), but reaches the execution engine through recipe.run_stream() with allow_dangerous_tools=False. This issue has been patched in version 4.6.59.","status":"published","published":"2026-10-23T11:58:01Z","modified":"2026-10-29T13:53:23Z","cvss":[{"version":"3.1","score":7.8,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"updated_at":"2026-11-02T16:59:16.484344Z"}],"ghsa":[{"id":"GHSA-v847-hxxw-3pxg","title":"PraisonAI recipe.run_stream skips dangerous-tool policy enforcement","description":"# PraisonAI `recipe.run_stream()` skips dangerous-tool policy enforcement\n\n## Summary\n\nPraisonAI recipe execution blocks default-denied dangerous tools unless the\ncaller explicitly passes `allow_dangerous_tools=True`. The normal `recipe.run()`\npath enforces this with `_check_tool_policy()`. The streaming path,\n`recipe.run_stream()`, loads the same recipe, checks dependencies, and then\ncalls `_execute_recipe()` without running the dangerous-tool policy check.\n\nAs a result, a recipe that honestly declares `execute_command` in\n`TEMPLATE.yaml requires.tools` is denied by `recipe.run()`, but reaches the\nexecution engine through `recipe.run_stream()` with\n`allow_dangerous_tools=False`.\n\nThe local PoV uses a harmless `printf` canary, explicitly unsets\n`PRAISONAI_AUTO_APPROVE`, and avoids network access.\n\n## Affected Product\n\n- Repository: `MervinPraison/PraisonAI`\n- Package: `praisonai`\n- Components:\n  - `src/praisonai/praisonai/recipe/core.py`\n  - `src/praisonai/praisonai/recipe/serve.py`\n  - `src/praisonai/praisonai/cli/features/recipe.py`\n  - `src/praisonai-agents/praisonaiagents/workflows/yaml_parser.py`\n  - `src/praisonai-agents/praisonaiagents/workflows/workflows.py`\n\nValidated affected:\n\n- current main `2f9677abb2ea68eab864ee8b6a828fd0141612e1`\n  (`v4.6.57-4-g2f9677ab`)\n- `v4.6.57`\n- `v4.6.56`\n- `v4.6.10`\n- `v4.6.9`\n- `v4.5.128`\n- `v4.5.120`\n- `v4.5.96`\n- `v4.5.87`\n\nSuggested affected range: `\u003e= 4.5.87, \u003c= 4.6.57`.\n\nPyPI lists `PraisonAI 4.6.57` as the latest release on 2026-06-13.\n\nEarlier tested tags through `v4.5.85` failed in this source checkout before the\ntested workflow path due an unrelated `praisonaiagents.output.models` import\nerror. They are not claimed fixed or unaffected.\n\n## Root Cause\n\n`recipe.run()` enforces the dangerous-tool gate:\n\n```python\nif not options.get(\"allow_dangerous_tools\", False):\n    policy_error = _check_tool_policy(recipe_config)\n    if policy_error:\n        return RecipeResult(..., status=RecipeStatus.POLICY_DENIED, ...)\n```\n\n`recipe.run_stream()` has a sibling execution path. It loads the recipe and\nchecks dependencies, but then goes directly to execution:\n\n```python\nrecipe_config = _load_recipe(name, offline=options.get(\"offline\", False))\n...\noutput = _execute_recipe(recipe_config, merged_config, session_id, options)\n```\n\nThere is no equivalent `_check_tool_policy()` call in `run_stream()` before\nexecution or before the dry-run shortcut.\n\nThe CLI exposes this path via `praisonai recipe run \u003crecipe\u003e --stream`, and the\nrecipe HTTP server exposes it as `POST /v1/recipes/stream`.\n\n## Why This Is Not Intended Behavior\n\nThe normal recipe path clearly treats declared dangerous tools as denied by\ndefault. A control recipe with `TEMPLATE.yaml requires.tools:\n[execute_command]` returns:\n\n```text\nTool 'execute_command' is denied by default. Use allow_dangerous_tools=True to override.\n```\n\nThat operator-facing override should not depend on whether the caller requests\nstreaming output. PraisonAI's own docs describe approval as requiring a human\nor configured channel before risky tools run, describe security environment\nvariables as opt-in access for dangerous operations with secure defaults, and\ndescribe policy controls as blocking dangerous operations.\n\nThis is distinct from the prior report `PRAI-CAND-011`:\n\n- `PRAI-CAND-011` covers workflow tool declarations that are omitted from\n  `TEMPLATE.yaml requires.tools`.\n- This report covers a sibling entrypoint that skips the policy check even when\n  `TEMPLATE.yaml` correctly declares the dangerous tool.\n\nIt is also distinct from the published Recipe-server authentication fail-open\nadvisory. That advisory covers missing authentication secrets. This report\nassumes the attacker has whatever access is already needed to invoke recipe\nstreaming and focuses on the missing dangerous-tool policy guard.\n\n## Local PoV\n\nRun:\n\n```bash\npython3 poc/pov_prai_cand_012_stream_policy_bypass.py\n```\n\nExpected output includes:\n\n```json\n{\n  \"ok\": true,\n  \"policy_error\": \"Tool 'execute_command' is denied by default. Use allow_dangerous_tools=True to override.\",\n  \"control_recipe_status\": \"policy_denied\",\n  \"execution_reached\": [\n    {\n      \"recipe\": \"declared-dangerous-stream\",\n      \"declared_required_tools\": [\"execute_command\"],\n      \"allow_dangerous_tools\": false\n    }\n  ],\n  \"workflow_approve_tools\": [\"execute_command\"],\n  \"runner_tool_names\": [\"execute_command\"],\n  \"command_stdout\": \"PRAI-CAND-012-CANARY\",\n  \"operator_env_auto_approve\": null\n}\n```\n\nThe PoV creates a temporary recipe that declares `execute_command` in\n`TEMPLATE.yaml requires.tools`.\n\nControl:\n\n- `recipe.run(..., options={\"force\": True})` returns `policy_denied`.\n\nBypass:\n\n- `recipe.run_stream(..., options={\"force\": True})` emits the `executing`\n  event and reaches `_execute_recipe()` while `allow_dangerous_tools` remains\n  false.\n- The same recipe workflow resolves `execute_command` and preserves\n  `approve: [execute_command]`.\n- With the workflow approval context installed, the resolved tool runs the\n  harmless local command `printf PRAI-CAND-012-CANARY`.\n\nThe PoV monkey-patches `_execute_recipe()` only to prove that\n`run_stream()` crosses the policy boundary without invoking an LLM. The command\ncanary is executed directly through the same resolved workflow tool and\napproval context to keep the proof deterministic and local-only.\n\n## Impact\n\nIf an operator runs an untrusted recipe through streaming mode, or exposes the\nrecipe streaming API to users who can choose recipe names or URIs, the recipe\ncan reach execution with default-denied tools even though the caller did not\nset `allow_dangerous_tools=True`.\n\nIf the workflow reaches the approved `execute_command` tool call, commands run\nwith the privileges of the PraisonAI process. The exact trigger depends on the\nworkflow and model/tool-call path, but the dangerous-tool policy boundary is\nalready bypassed before execution.\n\nThe HTTP recipe sidecar is documented as a localhost REST API with SSE\nstreaming and optional API-key/JWT authentication. This report does not claim\ndefault unauthenticated network RCE. In authenticated or exposed sidecar\ndeployments where lower-trust users can invoke `/v1/recipes/stream`, the same\npolicy gap can become a remote recipe-execution issue.\n\n## Suggested Fix\n\nCentralize recipe preflight enforcement so every execution mode uses the same\nguard:\n\n1. Run `_check_tool_policy(recipe_config)` in `run_stream()` unless\n   `options[\"allow_dangerous_tools\"]` is true.\n2. Perform that check before both dry-run and real execution, matching\n   `recipe.run()`.\n3. Prefer a shared helper for dependency checks, dangerous-tool policy checks,\n   and dry-run handling so future entrypoints cannot drift.\n4. Add regression tests:\n   - declared dangerous tool is denied by `recipe.run()`;\n   - the same declared dangerous tool is denied by `recipe.run_stream()`;\n   - `allow_dangerous_tools=True` preserves the intended opt-in behavior;\n   - `/v1/recipes/stream` maps a policy denial to a non-success SSE event or\n     equivalent HTTP failure.","status":"published","published":"2026-06-18T13:53:05Z","modified":"2026-07-20T21:23:42Z","cvss":[{"version":"3.1","score":7.8,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"cwes":["CWE-78","CWE-693","CWE-863"],"updated_at":"2026-11-02T14:40:48.410776Z"}],"nvd":[{"id":"CVE-2026-56838","description":"PraisonAI is a multi-agent teams system. From version 4.5.87 to before version 4.6.59, PraisonAI recipe execution blocks default-denied dangerous tools unless the caller explicitly passes allow_dangerous_tools=True. The normal recipe.run() path enforces this with _check_tool_policy(). The streaming path, recipe.run_stream(), loads the same recipe, checks dependencies, and then calls _execute_recipe() without running the dangerous-tool policy check. As a result, a recipe that honestly declares execute_command in TEMPLATE.yaml requires.tools is denied by recipe.run(), but reaches the execution engine through recipe.run_stream() with allow_dangerous_tools=False. This issue has been patched in version 4.6.59.","status":"published","published":"2026-10-23T12:18:15.357Z","modified":"2026-10-29T14:17:57.37Z","cvss":[{"version":"3.1","score":7.8,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high","assessor":"secondary"}],"cwes":["CWE-78","CWE-693","CWE-863"],"updated_at":"2026-11-02T14:13:02.724605Z"}],"osv":[{"id":"CVE-2026-56838","title":"PraisonAI recipe.run_stream skips dangerous-tool policy enforcement","description":"PraisonAI is a multi-agent teams system. From version 4.5.87 to before version 4.6.59, PraisonAI recipe execution blocks default-denied dangerous tools unless the caller explicitly passes allow_dangerous_tools=True. The normal recipe.run() path enforces this with _check_tool_policy(). The streaming path, recipe.run_stream(), loads the same recipe, checks dependencies, and then calls _execute_recipe() without running the dangerous-tool policy check. As a result, a recipe that honestly declares execute_command in TEMPLATE.yaml requires.tools is denied by recipe.run(), but reaches the execution engine through recipe.run_stream() with allow_dangerous_tools=False. This issue has been patched in version 4.6.59.","status":"published","published":"2026-10-23T11:58:01.475Z","modified":"2026-10-31T02:30:55.367040063Z","cvss":[{"version":"3.1","score":7.8,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"cwes":["CWE-693","CWE-78","CWE-863"],"updated_at":"2026-11-02T15:45:12.016252Z"},{"id":"GHSA-v847-hxxw-3pxg","title":"PraisonAI recipe.run_stream skips dangerous-tool policy enforcement","description":"# PraisonAI `recipe.run_stream()` skips dangerous-tool policy enforcement\n\n## Summary\n\nPraisonAI recipe execution blocks default-denied dangerous tools unless the\ncaller explicitly passes `allow_dangerous_tools=True`. The normal `recipe.run()`\npath enforces this with `_check_tool_policy()`. The streaming path,\n`recipe.run_stream()`, loads the same recipe, checks dependencies, and then\ncalls `_execute_recipe()` without running the dangerous-tool policy check.\n\nAs a result, a recipe that honestly declares `execute_command` in\n`TEMPLATE.yaml requires.tools` is denied by `recipe.run()`, but reaches the\nexecution engine through `recipe.run_stream()` with\n`allow_dangerous_tools=False`.\n\nThe local PoV uses a harmless `printf` canary, explicitly unsets\n`PRAISONAI_AUTO_APPROVE`, and avoids network access.\n\n## Affected Product\n\n- Repository: `MervinPraison/PraisonAI`\n- Package: `praisonai`\n- Components:\n  - `src/praisonai/praisonai/recipe/core.py`\n  - `src/praisonai/praisonai/recipe/serve.py`\n  - `src/praisonai/praisonai/cli/features/recipe.py`\n  - `src/praisonai-agents/praisonaiagents/workflows/yaml_parser.py`\n  - `src/praisonai-agents/praisonaiagents/workflows/workflows.py`\n\nValidated affected:\n\n- current main `2f9677abb2ea68eab864ee8b6a828fd0141612e1`\n  (`v4.6.57-4-g2f9677ab`)\n- `v4.6.57`\n- `v4.6.56`\n- `v4.6.10`\n- `v4.6.9`\n- `v4.5.128`\n- `v4.5.120`\n- `v4.5.96`\n- `v4.5.87`\n\nSuggested affected range: `\u003e= 4.5.87, \u003c= 4.6.57`.\n\nPyPI lists `PraisonAI 4.6.57` as the latest release on 2026-06-13.\n\nEarlier tested tags through `v4.5.85` failed in this source checkout before the\ntested workflow path due an unrelated `praisonaiagents.output.models` import\nerror. They are not claimed fixed or unaffected.\n\n## Root Cause\n\n`recipe.run()` enforces the dangerous-tool gate:\n\n```python\nif not options.get(\"allow_dangerous_tools\", False):\n    policy_error = _check_tool_policy(recipe_config)\n    if policy_error:\n        return RecipeResult(..., status=RecipeStatus.POLICY_DENIED, ...)\n```\n\n`recipe.run_stream()` has a sibling execution path. It loads the recipe and\nchecks dependencies, but then goes directly to execution:\n\n```python\nrecipe_config = _load_recipe(name, offline=options.get(\"offline\", False))\n...\noutput = _execute_recipe(recipe_config, merged_config, session_id, options)\n```\n\nThere is no equivalent `_check_tool_policy()` call in `run_stream()` before\nexecution or before the dry-run shortcut.\n\nThe CLI exposes this path via `praisonai recipe run \u003crecipe\u003e --stream`, and the\nrecipe HTTP server exposes it as `POST /v1/recipes/stream`.\n\n## Why This Is Not Intended Behavior\n\nThe normal recipe path clearly treats declared dangerous tools as denied by\ndefault. A control recipe with `TEMPLATE.yaml requires.tools:\n[execute_command]` returns:\n\n```text\nTool 'execute_command' is denied by default. Use allow_dangerous_tools=True to override.\n```\n\nThat operator-facing override should not depend on whether the caller requests\nstreaming output. PraisonAI's own docs describe approval as requiring a human\nor configured channel before risky tools run, describe security environment\nvariables as opt-in access for dangerous operations with secure defaults, and\ndescribe policy controls as blocking dangerous operations.\n\nThis is distinct from the prior report `PRAI-CAND-011`:\n\n- `PRAI-CAND-011` covers workflow tool declarations that are omitted from\n  `TEMPLATE.yaml requires.tools`.\n- This report covers a sibling entrypoint that skips the policy check even when\n  `TEMPLATE.yaml` correctly declares the dangerous tool.\n\nIt is also distinct from the published Recipe-server authentication fail-open\nadvisory. That advisory covers missing authentication secrets. This report\nassumes the attacker has whatever access is already needed to invoke recipe\nstreaming and focuses on the missing dangerous-tool policy guard.\n\n## Local PoV\n\nRun:\n\n```bash\npython3 poc/pov_prai_cand_012_stream_policy_bypass.py\n```\n\nExpected output includes:\n\n```json\n{\n  \"ok\": true,\n  \"policy_error\": \"Tool 'execute_command' is denied by default. Use allow_dangerous_tools=True to override.\",\n  \"control_recipe_status\": \"policy_denied\",\n  \"execution_reached\": [\n    {\n      \"recipe\": \"declared-dangerous-stream\",\n      \"declared_required_tools\": [\"execute_command\"],\n      \"allow_dangerous_tools\": false\n    }\n  ],\n  \"workflow_approve_tools\": [\"execute_command\"],\n  \"runner_tool_names\": [\"execute_command\"],\n  \"command_stdout\": \"PRAI-CAND-012-CANARY\",\n  \"operator_env_auto_approve\": null\n}\n```\n\nThe PoV creates a temporary recipe that declares `execute_command` in\n`TEMPLATE.yaml requires.tools`.\n\nControl:\n\n- `recipe.run(..., options={\"force\": True})` returns `policy_denied`.\n\nBypass:\n\n- `recipe.run_stream(..., options={\"force\": True})` emits the `executing`\n  event and reaches `_execute_recipe()` while `allow_dangerous_tools` remains\n  false.\n- The same recipe workflow resolves `execute_command` and preserves\n  `approve: [execute_command]`.\n- With the workflow approval context installed, the resolved tool runs the\n  harmless local command `printf PRAI-CAND-012-CANARY`.\n\nThe PoV monkey-patches `_execute_recipe()` only to prove that\n`run_stream()` crosses the policy boundary without invoking an LLM. The command\ncanary is executed directly through the same resolved workflow tool and\napproval context to keep the proof deterministic and local-only.\n\n## Impact\n\nIf an operator runs an untrusted recipe through streaming mode, or exposes the\nrecipe streaming API to users who can choose recipe names or URIs, the recipe\ncan reach execution with default-denied tools even though the caller did not\nset `allow_dangerous_tools=True`.\n\nIf the workflow reaches the approved `execute_command` tool call, commands run\nwith the privileges of the PraisonAI process. The exact trigger depends on the\nworkflow and model/tool-call path, but the dangerous-tool policy boundary is\nalready bypassed before execution.\n\nThe HTTP recipe sidecar is documented as a localhost REST API with SSE\nstreaming and optional API-key/JWT authentication. This report does not claim\ndefault unauthenticated network RCE. In authenticated or exposed sidecar\ndeployments where lower-trust users can invoke `/v1/recipes/stream`, the same\npolicy gap can become a remote recipe-execution issue.\n\n## Suggested Fix\n\nCentralize recipe preflight enforcement so every execution mode uses the same\nguard:\n\n1. Run `_check_tool_policy(recipe_config)` in `run_stream()` unless\n   `options[\"allow_dangerous_tools\"]` is true.\n2. Perform that check before both dry-run and real execution, matching\n   `recipe.run()`.\n3. Prefer a shared helper for dependency checks, dangerous-tool policy checks,\n   and dry-run handling so future entrypoints cannot drift.\n4. Add regression tests:\n   - declared dangerous tool is denied by `recipe.run()`;\n   - the same declared dangerous tool is denied by `recipe.run_stream()`;\n   - `allow_dangerous_tools=True` preserves the intended opt-in behavior;\n   - `/v1/recipes/stream` maps a policy denial to a non-success SSE event or\n     equivalent HTTP failure.","status":"published","published":"2026-06-18T13:53:05Z","modified":"2026-07-23T15:11:26.264669943Z","cvss":[{"version":"3.1","score":7.8,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"cwes":["CWE-693","CWE-78","CWE-863"],"updated_at":"2026-11-02T15:46:56.981655Z"},{"id":"PYSEC-2026-3520","title":"PraisonAI recipe.run_stream skips dangerous-tool policy enforcement","description":"# PraisonAI `recipe.run_stream()` skips dangerous-tool policy enforcement\n\n## Summary\n\nPraisonAI recipe execution blocks default-denied dangerous tools unless the\ncaller explicitly passes `allow_dangerous_tools=True`. The normal `recipe.run()`\npath enforces this with `_check_tool_policy()`. The streaming path,\n`recipe.run_stream()`, loads the same recipe, checks dependencies, and then\ncalls `_execute_recipe()` without running the dangerous-tool policy check.\n\nAs a result, a recipe that honestly declares `execute_command` in\n`TEMPLATE.yaml requires.tools` is denied by `recipe.run()`, but reaches the\nexecution engine through `recipe.run_stream()` with\n`allow_dangerous_tools=False`.\n\nThe local PoV uses a harmless `printf` canary, explicitly unsets\n`PRAISONAI_AUTO_APPROVE`, and avoids network access.\n\n## Affected Product\n\n- Repository: `MervinPraison/PraisonAI`\n- Package: `praisonai`\n- Components:\n  - `src/praisonai/praisonai/recipe/core.py`\n  - `src/praisonai/praisonai/recipe/serve.py`\n  - `src/praisonai/praisonai/cli/features/recipe.py`\n  - `src/praisonai-agents/praisonaiagents/workflows/yaml_parser.py`\n  - `src/praisonai-agents/praisonaiagents/workflows/workflows.py`\n\nValidated affected:\n\n- current main `2f9677abb2ea68eab864ee8b6a828fd0141612e1`\n  (`v4.6.57-4-g2f9677ab`)\n- `v4.6.57`\n- `v4.6.56`\n- `v4.6.10`\n- `v4.6.9`\n- `v4.5.128`\n- `v4.5.120`\n- `v4.5.96`\n- `v4.5.87`\n\nSuggested affected range: `\u003e= 4.5.87, \u003c= 4.6.57`.\n\nPyPI lists `PraisonAI 4.6.57` as the latest release on 2026-06-13.\n\nEarlier tested tags through `v4.5.85` failed in this source checkout before the\ntested workflow path due an unrelated `praisonaiagents.output.models` import\nerror. They are not claimed fixed or unaffected.\n\n## Root Cause\n\n`recipe.run()` enforces the dangerous-tool gate:\n\n```python\nif not options.get(\"allow_dangerous_tools\", False):\n    policy_error = _check_tool_policy(recipe_config)\n    if policy_error:\n        return RecipeResult(..., status=RecipeStatus.POLICY_DENIED, ...)\n```\n\n`recipe.run_stream()` has a sibling execution path. It loads the recipe and\nchecks dependencies, but then goes directly to execution:\n\n```python\nrecipe_config = _load_recipe(name, offline=options.get(\"offline\", False))\n...\noutput = _execute_recipe(recipe_config, merged_config, session_id, options)\n```\n\nThere is no equivalent `_check_tool_policy()` call in `run_stream()` before\nexecution or before the dry-run shortcut.\n\nThe CLI exposes this path via `praisonai recipe run \u003crecipe\u003e --stream`, and the\nrecipe HTTP server exposes it as `POST /v1/recipes/stream`.\n\n## Why This Is Not Intended Behavior\n\nThe normal recipe path clearly treats declared dangerous tools as denied by\ndefault. A control recipe with `TEMPLATE.yaml requires.tools:\n[execute_command]` returns:\n\n```text\nTool 'execute_command' is denied by default. Use allow_dangerous_tools=True to override.\n```\n\nThat operator-facing override should not depend on whether the caller requests\nstreaming output. PraisonAI's own docs describe approval as requiring a human\nor configured channel before risky tools run, describe security environment\nvariables as opt-in access for dangerous operations with secure defaults, and\ndescribe policy controls as blocking dangerous operations.\n\nThis is distinct from the prior report `PRAI-CAND-011`:\n\n- `PRAI-CAND-011` covers workflow tool declarations that are omitted from\n  `TEMPLATE.yaml requires.tools`.\n- This report covers a sibling entrypoint that skips the policy check even when\n  `TEMPLATE.yaml` correctly declares the dangerous tool.\n\nIt is also distinct from the published Recipe-server authentication fail-open\nadvisory. That advisory covers missing authentication secrets. This report\nassumes the attacker has whatever access is already needed to invoke recipe\nstreaming and focuses on the missing dangerous-tool policy guard.\n\n## Local PoV\n\nRun:\n\n```bash\npython3 poc/pov_prai_cand_012_stream_policy_bypass.py\n```\n\nExpected output includes:\n\n```json\n{\n  \"ok\": true,\n  \"policy_error\": \"Tool 'execute_command' is denied by default. Use allow_dangerous_tools=True to override.\",\n  \"control_recipe_status\": \"policy_denied\",\n  \"execution_reached\": [\n    {\n      \"recipe\": \"declared-dangerous-stream\",\n      \"declared_required_tools\": [\"execute_command\"],\n      \"allow_dangerous_tools\": false\n    }\n  ],\n  \"workflow_approve_tools\": [\"execute_command\"],\n  \"runner_tool_names\": [\"execute_command\"],\n  \"command_stdout\": \"PRAI-CAND-012-CANARY\",\n  \"operator_env_auto_approve\": null\n}\n```\n\nThe PoV creates a temporary recipe that declares `execute_command` in\n`TEMPLATE.yaml requires.tools`.\n\nControl:\n\n- `recipe.run(..., options={\"force\": True})` returns `policy_denied`.\n\nBypass:\n\n- `recipe.run_stream(..., options={\"force\": True})` emits the `executing`\n  event and reaches `_execute_recipe()` while `allow_dangerous_tools` remains\n  false.\n- The same recipe workflow resolves `execute_command` and preserves\n  `approve: [execute_command]`.\n- With the workflow approval context installed, the resolved tool runs the\n  harmless local command `printf PRAI-CAND-012-CANARY`.\n\nThe PoV monkey-patches `_execute_recipe()` only to prove that\n`run_stream()` crosses the policy boundary without invoking an LLM. The command\ncanary is executed directly through the same resolved workflow tool and\napproval context to keep the proof deterministic and local-only.\n\n## Impact\n\nIf an operator runs an untrusted recipe through streaming mode, or exposes the\nrecipe streaming API to users who can choose recipe names or URIs, the recipe\ncan reach execution with default-denied tools even though the caller did not\nset `allow_dangerous_tools=True`.\n\nIf the workflow reaches the approved `execute_command` tool call, commands run\nwith the privileges of the PraisonAI process. The exact trigger depends on the\nworkflow and model/tool-call path, but the dangerous-tool policy boundary is\nalready bypassed before execution.\n\nThe HTTP recipe sidecar is documented as a localhost REST API with SSE\nstreaming and optional API-key/JWT authentication. This report does not claim\ndefault unauthenticated network RCE. In authenticated or exposed sidecar\ndeployments where lower-trust users can invoke `/v1/recipes/stream`, the same\npolicy gap can become a remote recipe-execution issue.\n\n## Suggested Fix\n\nCentralize recipe preflight enforcement so every execution mode uses the same\nguard:\n\n1. Run `_check_tool_policy(recipe_config)` in `run_stream()` unless\n   `options[\"allow_dangerous_tools\"]` is true.\n2. Perform that check before both dry-run and real execution, matching\n   `recipe.run()`.\n3. Prefer a shared helper for dependency checks, dangerous-tool policy checks,\n   and dry-run handling so future entrypoints cannot drift.\n4. Add regression tests:\n   - declared dangerous tool is denied by `recipe.run()`;\n   - the same declared dangerous tool is denied by `recipe.run_stream()`;\n   - `allow_dangerous_tools=True` preserves the intended opt-in behavior;\n   - `/v1/recipes/stream` maps a policy denial to a non-success SSE event or\n     equivalent HTTP failure.","status":"published","published":"2026-07-23T11:41:40.808701Z","modified":"2026-07-23T15:00:20.940946166Z","cvss":[{"version":"3.1","score":7.8,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","level":"high"}],"updated_at":"2026-11-02T15:52:56.847083Z"}]}}
