From 36a91a2451bd01539cb357e45ebbb751973d0e2c Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 13 Oct 2026 04:51:58 +0300 Subject: [PATCH] fix: remove builder proxy declarations from exported package environments --- hydra/image.py | 38 ++++++++++++++++++++++++++++++++++++++ tests/test_image.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/hydra/image.py b/hydra/image.py index cde36f9..1ca571c 100644 --- a/hydra/image.py +++ b/hydra/image.py @@ -1,5 +1,8 @@ """Prepare only exported Gentoo payloads and configure the dedicated guest network.""" import argparse +import bz2 +import io +import runpy import hashlib import ipaddress import json @@ -50,6 +53,39 @@ def write(root, relative, content, mode=0o644): return path +def sanitize_saved_environments(root): + """Use Portage's parser to scrub exported declarations without evaluating them.""" + database = root_path(root, 'var/db/pkg') + paths = sorted(database.glob('*/*/environment.bz2')) + result = {'files_scanned': len(paths), 'filtered_files': 0, 'variable_names': []} + if not paths: + return result + scripts = sorted(root_path(root, 'usr/lib/portage').glob('*/filter-bash-environment.py')) + if not scripts: + raise ImageError('Installed Portage environment filter is required for export') + script = root_path(root, scripts[0].relative_to(root)) + api = runpy.run_path(str(script)) + pattern = re.compile(rb'(?i:(?:HTTP|HTTPS|FTP|ALL|NO)_PROXY|.*(?:PASSWORD|PASSWD|SECRET|TOKEN|API_KEY|ACCESS_KEY|CREDENTIAL|AUTHORIZATION).*)') + declarations = re.compile(rb'^(?:declare|export|readonly)\s+(?:-[^ ]+\s+)*([A-Za-z_][A-Za-z0-9_]*)=', re.M) + names = set() + for path in paths: + path = root_path(root, path.relative_to(root)) + try: + original = bz2.decompress(path.read_bytes()) + except (OSError, ValueError) as error: + raise ImageError('Cannot read an exported package environment') from error + filtered = io.BytesIO() + api['filter_bash_environment'](pattern, io.BytesIO(original), filtered) + clean = filtered.getvalue() + if clean != original: + names.update(name.decode('ascii') for name in declarations.findall(original) + if pattern.fullmatch(name)) + path.write_bytes(bz2.compress(clean)) + result['filtered_files'] += 1 + result['variable_names'] = sorted(names) + return result + + def sanitize_rootfs(root): """Erase builder identity/secrets in the exported copy, retaining Gentoo updates.""" for relative in ('etc/portage/gnupg', 'etc/portage/gnupg.hydra-untrusted', @@ -76,6 +112,8 @@ def sanitize_rootfs(root): write(root, 'etc/hosts', '127.0.0.1 localhost\n::1 localhost\n127.0.1.1 hydra-os\n') remove(root, 'etc/resolv.conf') root_path(root, 'etc/resolv.conf', follow_leaf=False).symlink_to('/run/NetworkManager/resolv.conf') + audit = sanitize_saved_environments(root) + write(root, 'usr/share/hydra/environment-sanitization.json', json.dumps(audit, indent=2) + '\n') def copy_overlay(root, overlay): diff --git a/tests/test_image.py b/tests/test_image.py index e68507b..de09ecd 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -1,4 +1,5 @@ import importlib.util +import bz2 import json import os import shlex @@ -73,6 +74,36 @@ class ImageTests(unittest.TestCase): self.assertEqual((self.root / 'etc/hostname').read_text(), 'hydra-os\n') self.assertTrue((self.root / 'etc/resolv.conf').is_symlink()) + def test_export_filters_saved_environment_proxies_but_retains_package_metadata(self): + self.assertTrue(callable(getattr(image, 'sanitize_saved_environments', None)), 'saved environment scrubber is missing') + # Portable stand-in for the external Portage filter API, not the scrubber. + helper = self.file('usr/lib/portage/python3.14/filter-bash-environment.py', + "import re\ndef filter_bash_environment(pattern, source, output):\n" + " for line in source:\n" + " match = re.match(rb'declare -x ([A-Za-z_][A-Za-z0-9_]*)=', line)\n" + " if not match or not pattern.fullmatch(match.group(1)):\n" + " output.write(line)\n") + path = self.root / 'var/db/pkg/app-emulation/qemu-9.2.4/environment.bz2' + path.parent.mkdir(parents=True) + package_metadata = b'declare -x LICENSE="GPL-2"\ndeclare -x EAPI="8"\n' + path.write_bytes(bz2.compress(package_metadata + b'declare -x HTTPS_PROXY="https://user:secret@builder.invalid"\n' + b'declare -x HTTP_PROXY="http://127.0.0.1:1234"\n' + b'declare -x NO_PROXY="localhost"\n')) + result = image.sanitize_saved_environments(self.root) + self.assertEqual(bz2.decompress(path.read_bytes()), package_metadata) + self.assertEqual(result['filtered_files'], 1) + self.assertEqual(result['variable_names'], ['HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY']) + + def test_saved_environment_filter_refuses_missing_portage_helper(self): + self.assertTrue(callable(getattr(image, 'sanitize_saved_environments', None)), 'saved environment scrubber is missing') + path = self.root / 'var/db/pkg/app-emulation/qemu-9.2.4/environment.bz2' + path.parent.mkdir(parents=True) + original = bz2.compress(b'declare -x HTTPS_PROXY="private"\n') + path.write_bytes(original) + with self.assertRaises(image.ImageError): + image.sanitize_saved_environments(self.root) + self.assertEqual(path.read_bytes(), original) + def test_sanitization_cannot_follow_exported_symlink_to_host(self): outside = Path(self.tmp.name) / 'host' outside.mkdir()