From ead697d04908748aedf97f9efb60a16fb5a69bcc Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 22 Jul 2026 18:36:12 +0200 Subject: [PATCH] fix(assessment): constrain installed record reads --- tests/test_capability_fit_evidence.py | 23 +++++++----- .../govoplan_assessment/evidence.py | 37 ++++++++++++++++--- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/tests/test_capability_fit_evidence.py b/tests/test_capability_fit_evidence.py index 0b12378..9ffc880 100644 --- a/tests/test_capability_fit_evidence.py +++ b/tests/test_capability_fit_evidence.py @@ -11,6 +11,7 @@ import sys import tempfile from types import SimpleNamespace import unittest +from unittest import mock from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey @@ -217,11 +218,12 @@ class CapabilityFitEvidenceTests(unittest.TestCase): root = Path(temp_dir) distribution, payload_file = create_distribution(root) - evidence = collect_installed_composition( - assessment=self.assessment, - collected_at=datetime(2026, 7, 22, 12, tzinfo=UTC), - distributions=[distribution], - ) + with mock.patch.object(sys, "path", [str(root), *sys.path]): + evidence = collect_installed_composition( + assessment=self.assessment, + collected_at=datetime(2026, 7, 22, 12, tzinfo=UTC), + distributions=[distribution], + ) self.assertEqual( (), validate_payload(payload=evidence, schema=self.installed_schema) @@ -234,11 +236,12 @@ class CapabilityFitEvidenceTests(unittest.TestCase): self.assertNotIn("file://", encoded) payload_file.write_text("tampered\n", encoding="utf-8") - tampered = collect_installed_composition( - assessment=self.assessment, - collected_at=datetime(2026, 7, 22, 12, tzinfo=UTC), - distributions=[distribution], - ) + with mock.patch.object(sys, "path", [str(root), *sys.path]): + tampered = collect_installed_composition( + assessment=self.assessment, + collected_at=datetime(2026, 7, 22, 12, tzinfo=UTC), + distributions=[distribution], + ) self.assertEqual( "mismatch", tampered["artifacts"][0]["record_integrity"]["status"] diff --git a/tools/assessments/govoplan_assessment/evidence.py b/tools/assessments/govoplan_assessment/evidence.py index 0fe0ade..a5b8713 100644 --- a/tools/assessments/govoplan_assessment/evidence.py +++ b/tools/assessments/govoplan_assessment/evidence.py @@ -9,7 +9,7 @@ import hashlib import hmac from importlib import metadata import json -from pathlib import Path +from pathlib import Path, PurePosixPath import re import sys from typing import Any, Iterable, Mapping, Sequence @@ -1080,6 +1080,12 @@ def _record_integrity( installation_root = Path(sys.prefix).resolve() except OSError: return {"status": "unavailable", **counts} + if not _trusted_distribution_root(distribution_root): + return {"status": "unavailable", **counts} + script_roots = ( + (installation_root / "bin").resolve(), + (installation_root / "Scripts").resolve(), + ) total_hashed_bytes = 0 limit_exceeded = False for package_path in files: @@ -1098,10 +1104,18 @@ def _record_integrity( except OSError: counts["missing_file_count"] += 1 continue - if not ( - _is_relative_to(resolved_path, distribution_root) - or _is_relative_to(resolved_path, installation_root) - ): + record_path = PurePosixPath(str(package_path).replace("\\", "/")) + has_parent_traversal = ".." in record_path.parts + path_allowed = ( + not record_path.is_absolute() + and not has_parent_traversal + and _is_relative_to(resolved_path, distribution_root) + ) or ( + not record_path.is_absolute() + and has_parent_traversal + and any(_is_relative_to(resolved_path, root) for root in script_roots) + ) + if not path_allowed: counts["unverifiable_file_count"] += 1 continue try: @@ -1320,3 +1334,16 @@ def _is_relative_to(path: Path, root: Path) -> bool: except ValueError: return False return True + + +def _trusted_distribution_root(root: Path) -> bool: + for value in sys.path: + try: + candidate = Path(value or ".").resolve() + except OSError: + continue + if candidate.parent == candidate: + continue + if root == candidate or _is_relative_to(root, candidate): + return True + return False