fix(assessment): constrain installed record reads

This commit is contained in:
2026-07-22 18:36:12 +02:00
parent 65aa8e0b7c
commit ead697d049
2 changed files with 45 additions and 15 deletions

View File

@@ -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,6 +218,7 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
root = Path(temp_dir)
distribution, payload_file = create_distribution(root)
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),
@@ -234,6 +236,7 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
self.assertNotIn("file://", encoded)
payload_file.write_text("tampered\n", encoding="utf-8")
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),

View File

@@ -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