GHSA-f44v-7qgw-9gh9 PraisonAI GitHub template cache path traversal allows outside-cache file write and directory deletion
SummaryPraisonAI's template loader accepts GitHub template URIs with refs, for example
`github:owner/repo/template@v1.0.0`. The resolver stores the user-controlled
template path and ref verbatim, and the cache layer later joins those values into
`~/.praison/cache/templates/github/<owner>/<repo>/<template>/<ref>` without
normalizing each segment or checking that the final path remains inside the
template cache root.A crafted ref such as `../../../../../../outside-delete-target` therefore
escapes the cache directory. The first load can write `.cache_meta.json` outside
the cache. If the normal cache hierarchy for the same owner/repo/template has
already been created, the same path reaches `shutil.rmtree(cache_path)` and
removes an attacker-selected outside directory before replacing it with cache
metadata.This is distinct from the old template Zip Slip advisory. No malicious archive
member is needed, and the PoV disables network access entirely. The bug is in
cache-key construction for GitHub template URIs.Affected versionsConfirmed vulnerable:`v2.6.0``v3.9.24``v3.9.26``v4.5.126``v4.5.128``v4.6.9``v4.6.10``v4.6.56``v4.6.57`current head `2f9677abb2ea68eab864ee8b6a828fd0141612e1`Recommended affected range: `>= 2.6.0, <= 4.6.57`.No fixed version is known at the time of this report.ImpactAn attacker who can cause a user or service to load an attacker-supplied
PraisonAI GitHub template URI can:create `.cache_meta.json` outside the template cache directory;delete a directory reachable by the PraisonAI process after a normal cache
entry exists for the same owner/repo/template prefix;corrupt user configuration, project state, or application data reachable by
the process permissions.Root causeCurrent-head code path:`praisonai/templates/resolver.py`: `GITHUB_PATTERN` captures `path` and `ref`
with broad regex groups and returns them without segment validation.`praisonai/templates/security.py`: `is_source_allowed()` allows GitHub sources
by default when `allow_any_github` is true.`praisonai/templates/registry.py`: `get_template()` resolves a GitHub URI,
fetches the template, calculates a checksum, then calls `self.cache.put(...)`.`praisonai/templates/cache.py`: `_get_cache_path()` builds the cache path as
`self.cache_dir / "github" / resolved.owner / resolved.repo /
resolved.path / ref`.`praisonai/templates/cache.py`: `put()` removes an existing `cache_path` with
`shutil.rmtree(cache_path)`, recreates it, copies content, and writes
`.cache_meta.json`.There is no check equivalent to:reject absolute path segments;reject `.` / `..` in owner, repo, template path, or ref;resolve the candidate path;require `os.path.commonpath([cache_root, candidate]) == cache_root`.Local-only PoVRun from a PraisonAI source checkout:Expected output:The PoV uses only temporary directories and disables network fetches.I also confirmed the same behavior without monkeypatching network fetches. With
a non-existent GitHub repository, PraisonAI makes real GitHub requests, handles
the failed fetch, returns a fallback template config, and still writes/deletes
through the escaped cache path. The PoV above disables network only to keep the
reproducer deterministic and harmless.Release sweepThe same PoV was run against checked-out tags:`git log` shows the affected template cache/resolver/registry files were added
in the `v2.6.0` release commit `e7a8ce8e`.Suggested fixValidate every cache path segment before joining:owner and repo: strict GitHub owner/repo-name regex;template path: split on `/` and reject empty, `.`, `..`, and absolute forms;ref: reject `/`, path separators, empty segments, `.`, `..`, and absolute
forms, or encode/hash the ref before using it in a filesystem path.Then enforce a final boundary check:A more robust design is to hash untrusted URI fields into opaque directory names
instead of using raw remote identifiers as path segments.Also consider failing closed when a GitHub template fetch returns no files.
Currently a failed fetch can still result in a cached empty template directory.
CVSS v3.1 score: 8.1