{"id":"CVE-2026-60089","aliases":["GHSA-qjw5-xwrp-xwpq"],"title":"PraisonAI: Project config can auto-save agent output outside the project root","summary":"PraisonAI: Project config can auto-save agent output outside the project root","severity":"medium","cwe":["CWE-22","CWE-73"],"vendor":"praisonaiagents","product":"praisonaiagents","ecosystem":"pip","affected":["praisonaiagents <= 1.6.77"],"patched":["praisonaiagents 1.6.78"],"published":"2026-10-08","updated":"2026-10-08","sourceUpdated":"2026-10-08T16:36:35Z","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-qjw5-xwrp-xwpq","references":[{"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-qjw5-xwrp-xwpq"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-60089"},{"url":"https://github.com/MervinPraison/PraisonAI/commit/3aa9cbc2bd49c23a32be0a89a5e620d13d843eab"},{"url":"https://www.vulncheck.com/advisories/praisonai-before-path-traversal-via-config-toml"},{"url":"https://github.com/advisories/GHSA-qjw5-xwrp-xwpq"}],"tags":["ghsa","pip"],"epss":0.0018,"epssPercentile":0.0688,"ingestedAt":"2026-10-08T16:52:14.782Z","slug":"CVE-2026-60089","body":"## Overview\n\n# Project config can auto-save agent output outside the project root\n\n## Summary\n\n`praisonaiagents` automatically reads project-local `.praisonai/config.toml` defaults when constructing an `Agent`. A repository-controlled config can set `defaults.output.output_file` to an absolute path or a `..` traversal path. When the developer later calls `agent.start(...)`, PraisonAI writes the agent response to that path with `open(..., \"w\")`, creating parent directories if needed.\n\nThis lets an untrusted project overwrite files outside the project root with the privileges of the user running PraisonAI.\n\n## Technical Details\n\nThe source-to-sink path is `Agent.__init__()` project config loading to `OutputConfig.output_file` to public `agent.start()` output auto-save. `praisonaiagents/agent/agent.py` applies config-driven defaults before parameter resolution; if the caller did not explicitly pass `output`, it calls `apply_config_defaults(\"output\", output, OutputConfig)`. `praisonaiagents/config/loader.py` treats a config block with `enabled = true` as active and instantiates `OutputConfig` from the remaining keys. `OutputConfig` includes `output_file`, and the agent stores that value as `self._output_file`.\n\nAfter `agent.start(...)` obtains a truthy result from `self.chat(...)`, `praisonaiagents/agent/execution_mixin.py` calls `_save_output_to_file(str(result))` when `self._output_file` is set. `praisonaiagents/agent/memory_mixin.py` then runs `expanduser()` and `abspath()`, creates parent directories, and writes the destination with mode `w`. It does not constrain the resolved path to the current project, reject absolute paths, reject `..`, or distinguish an output path explicitly chosen by trusted application code from one loaded out of a project-local config file.\n\nThis is not a claim that explicit `Agent(output=OutputConfig(output_file=...))` chosen by trusted application code is unsafe by itself. The security boundary crossed here is the automatically consumed project-local config file: a checked-out project can steer the write destination without the application code opting into that path.\n\n## PoV\n\nCreate a project containing:\n\n```toml\n[defaults.output]\nenabled = true\noutput_file = \"../victim-outside-project/agent-output.txt\"\n```\n\nThen run ordinary agent code from inside that project without passing an explicit `output` parameter. The resolved output path escapes the project root, and PraisonAI writes the agent response there after `agent.start(...)`.\n\nI verified this locally without any external model call by replacing `agent.chat` with a deterministic offline stub after constructing the real `Agent`; the public `start()` method still performed the auto-save. Current-head output:\n\n```json\n{\n  \"configured_output_file\": \"../victim-outside-project/agent-output.txt\",\n  \"escaped_project_root\": true,\n  \"source_head\": \"3aa9cbc2bd49c23a32be0a89a5e620d13d843eab\",\n  \"start_returned\": true,\n  \"canary_written\": true\n}\n```\n\nNegative controls:\n\n```json\n[\n  {\n    \"case\": \"safe-relative\",\n    \"configured_output_file\": \"inside-output.txt\",\n    \"expected_file_escaped_project\": false,\n    \"expected_file_exists\": true,\n    \"observed_files\": {\n      \"project/inside-output.txt\": \"PRAISONAI_NEGATIVE_CONTROL_safe-relative\\n\"\n    },\n    \"outside_files\": [],\n    \"start_returned\": true\n  },\n  {\n    \"case\": \"disabled-output\",\n    \"configured_output_file\": null,\n    \"expected_file_escaped_project\": null,\n    \"expected_file_exists\": false,\n    \"observed_files\": {},\n    \"outside_files\": [],\n    \"start_returned\": true\n  }\n]\n```\n\nThe first control shows a safe relative output path stays inside the project. The second control shows a traversal `output_file` is not applied when `defaults.output.enabled` is false.\n\n## PoC\n\n```python\n#!/usr/bin/env python3\nimport os\nimport shutil\nfrom pathlib import Path\n\nfrom praisonaiagents import Agent\nfrom praisonaiagents.config.loader import clear_config_cache\n\nwork = Path(\"praison-outputfile-poc\").resolve()\nproject = work / \"untrusted-project\"\nvictim = work / \"victim-outside-project\" / \"agent-output.txt\"\n\nshutil.rmtree(work, ignore_errors=True)\n(project / \".praisonai\").mkdir(parents=True)\nvictim.parent.mkdir(parents=True)\n\n(project / \".praisonai\" / \"config.toml\").write_text(\n    \"[defaults.output]\\n\"\n    \"enabled = true\\n\"\n    'output_file = \"../victim-outside-project/agent-output.txt\"\\n',\n    encoding=\"utf-8\",\n)\n\nos.chdir(project)\nclear_config_cache()\n\nagent = Agent(instructions=\"offline PoC\")\nagent.chat = lambda prompt, **kwargs: \"PRAISONAI_OUTPUTFILE_CANARY\\n\"\nagent.start(\"offline prompt\")\n\nprint(victim.read_text(encoding=\"utf-8\"))\nprint(victim.resolve())\n```\n\nExpected affected result:\n\n- `victim-outside-project/agent-output.txt` is created outside `untrusted-project`.\n- The file contains `PRAISONAI_OUTPUTFILE_CANARY`.\n\n## Impact\n\nA malicious repository can cause PraisonAI to truncate and replace files outside the repository when a developer runs agent code from that directory. The write is limited to the permissions of the local user, but that commonly includes dotfiles, project-adjacent files, CI workspace files, and other user-writable paths.\n\nThe content written is the agent response rather than arbitrary bytes in the strictest sense. However, the same untrusted project can influence the agent prompt/config context, and the primitive is still an unintended file overwrite outside the project boundary.\n\n## Suggested Fix\n\nTreat `output_file` loaded from project-local config as untrusted:\n\n- Resolve project-configured `output_file` relative to the project root and reject paths that escape that root after symlink-aware normalization.\n- Reject absolute paths and `..` traversal in project config by default.\n- Preserve existing behavior for explicit trusted application code, for example `Agent(output=OutputConfig(output_file=...))`, or require an explicit `allow_external_output_file` opt-in for config-sourced paths.\n- Avoid creating parent directories outside the allowed root for config-sourced output.\n- Add regression tests for `.praisonai/config.toml` with relative traversal, absolute paths, and symlinked parent directories.\n\n## Affected Package/Versions\n\nConfirmed affected:\n\n- GitHub current head `3aa9cbc2bd49c23a32be0a89a5e620d13d843eab`.\n- `praisonaiagents` 1.6.64, latest PyPI release at test time.\n- `praisonaiagents` 1.6.63, previous PyPI release tested.\n\nThe `praisonai` package version 4.6.64 depends on `praisonaiagents>=1.6.64`, so `praisonai` users can receive the affected code transitively when they use the `praisonaiagents.Agent` path.\n\n## Advisory History\n\nI did not find an existing advisory summary for `output_file` / `OutputConfig` / `defaults.output` project-configured output path escape in the repository advisory list.\n\nRelated but distinct advisories exist for other PraisonAI path traversal, file-write, file-read, and tool boundary issues. This report covers the `praisonaiagents` project config to `OutputConfig.output_file` auto-save path.\n\nNo public disclosure or external submission was performed as part of this report preparation.\n\n## References\n\n- `praisonaiagents/agent/agent.py`: config defaults are applied to `output`, then `output_file` is stored on the agent.\n- `praisonaiagents/config/loader.py`: enabled config defaults instantiate the requested config class.\n- `praisonaiagents/config/feature_configs.py`: `OutputConfig.output_file`.\n- `praisonaiagents/agent/execution_mixin.py`: `start()` auto-saves agent output.\n- `praisonaiagents/agent/memory_mixin.py`: `_save_output_to_file()` resolves and writes the configured path without project containment.\n\n## Affected packages\n\n- `praisonaiagents <= 1.6.77`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `praisonaiagents 1.6.78`","depth":"sunlit","depthScore":28,"depthScoreParts":{"impact":27.5,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}