GitHub Advisoryadvisory · PyPI
GHSA-q7m5-3jmv-vm48 PraisonAI: ContextGatherer include resolution permits absolute and traversal reads outside the workspace
MEDIUM
Official page ↗
- Aliases
- CVE-2026-61431
- Ecosystem
- PyPI
- Packages
- praisonai
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
- Weaknesses
- CWE-200, CWE-22
- Reviewed
- 2026-10-08
Affected packages
• PyPI praisonai: < 4.6.78 → fixed in 4.6.78
Description
ContextGatherer include resolution permits absolute and traversal reads outside the workspace
Summary
PraisonAI's `praisonai.ui.context.ContextGatherer` treats the configured `directory` as the project workspace, but project-controlled `.praisoncontext` and `.praisoninclude` files can name absolute paths or `..` traversal paths. When context gathering runs, PraisonAI opens those outside paths and appends their contents to the generated context bundle. An attacker who can supply or modify a workspace repository can therefore cause process-readable files outside the intended project root to be sent to the caller or model as project context.
Technical Details
`ContextGatherer.get_include_paths()` reads include entries directly from `.praisoncontext` and `.praisoninclude` under the configured workspace. It stores each non-comment line as a raw include path:
```python
include_file = os.path.join(self.directory, '.praisoncontext')
if os.path.exists(include_file):
with open(include_file, 'r') as f:
include_paths.extend(
line.strip() for line in f
if line.strip() and not line.startswith('#')
)
```
When `.praisoncontext` is present, `gather_context()` passes every include entry through `os.path.join(self.directory, include_path)` and then processes the result:
```python
for include_path in self.include_paths:
full_path = os.path.join(self.directory, include_path)
process_path(full_path)
```
The `.praisoninclude` path has the same unsafe join after first processing the workspace:
```python
process_path(self.directory)
for include_path in self.include_paths:
full_path = os.path.join(self.directory, include_path)
process_path(full_path)
```
There is no canonicalization or containment check before `process_path()` opens files or recursively walks directories. In Python, `os.path.join(workspace, absolute_path)` returns the absolute path and discards `workspace`; `os.path.join(workspace, "../outside.py")` remains outside the workspace once normalized by filesystem operations. `add_file_content()` then opens the supplied path and appends file contents to the context before display bookkeeping:
```python
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
context.append(
f"File: {file_path}\n\n{content}\n\n{'=' * 50}\n"
)
self.included_files.append(
Path(file_path).relative_to(self.directory)
)
```
For parent traversal paths, `Path(file_path).relative_to(self.directory)` raises after the outside file content has already been appended, so the caller receives the outside content even if an error is logged. For absolute paths, the outside content is appended as well. This violates the workspace invariant for a context-gathering feature: repository-local include metadata should select files within the project, not arbitrary process-readable host files.
PoV
The minimal vulnerable shape is a workspace containing only a normal source file and one include file:
```text
workspace/
.praisoncontext # contains: ../outside_secret.py
inside.py
outside_secret.py # outside the workspace
```
Running `ContextGatherer(directory="workspace").run()` returns context containing `outside_secret.py` even though that file is outside the configured workspace. The same result occurs when `.praisoncontext` contains an absolute path to the outside file, and when `.praisoninclude` contains either the parent traversal path or the absolute path.
PoC
Save the self-contained script from the Appendix below as `context_include_workspace_pov.py`, then run it against a local checkout:
```bash
export PRAISONAI=/path/to/PraisonAI
PYTHONPATH="$PRAISONAI/src/praisonai" python context_include_workspace_pov.py
```
Expected vulnerable output:
```json
{
"expectations": {
"control_inside_file_is_collected": true,
"control_without_include_does_not_read_outside": true,
"praisoncontext_absolute_path_discloses_outside": true,
"praisoncontext_parent_traversal_discloses_outside": true,
"praisoninclude_absolute_path_discloses_outside": true,
"praisoninclude_parent_traversal_discloses_outside": true
},
"source_commit": "1620b49f36945d8cc8ee5635b906c960df5097a0",
"source_file": "$PRAISONAI/src/praisonai/praisonai/ui/context.py",
"vulnerable": true
}
```
The version sweep sampled old and current releases. All sampled versions are vulnerable:
```text
{"ref":"v2.3.10","praisonai_version":"2.3.10","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
{"ref":"v2.3.11","praisonai_version":"2.3.11","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
{"ref":"v3.8.1","praisonai_version":"3.8.1","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
{"ref":"v3.9.26","praisonai_version":"3.9.26","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
{"ref":"v4.4.12","praisonai_version":"4.4.12","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
{"ref":"v4.5.16","praisonai_version":"4.5.16","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
{"ref":"v4.5.128","praisonai_version":"4.5.128","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
{"ref":"v4.6.58","praisonai_version":"4.6.58","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
{"ref":"v4.6.62","praisonai_version":"4.6.62","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
{"ref":"v4.6.63","praisonai_version":"4.6.63","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
{"ref":"HEAD","praisonai_version":"4.6.63","status":"vulnerable","control_without_include_does_not_read_outside":true,"relative_praisoncontext_discloses_outside":true,"absolute_praisoncontext_discloses_outside":true,"relative_praisoninclude_discloses_outside":true,"absolute_praisoninclude_discloses_outside":true}
```
No external service, live target, real credential, model prov…
Related
Alias · CVE (1)
- CVE-2026-61431 PraisonAI before 4.6.78 contains a path traversal vulnerability in ContextGatherer that fails to validate include paths in .praisoncontext… vulnerability
References
- https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-q7m5-3jmv-vm48 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-61431 advisory
- https://github.com/MervinPraison/PraisonAI/commit/1620b49f36945d8cc8ee5635b906c960df5097a0 web
- https://github.com/MervinPraison/PraisonAI package
- https://www.vulncheck.com/advisories/praisonai-before-path-traversal-via-contextgatherer web