Harden isolated release builds in Flatpak
Some checks failed
Dependency Audit / dependency-audit (push) Failing after 16s
Security Audit / security-audit (push) Failing after 15s

This commit is contained in:
2026-07-22 21:48:48 +02:00
parent 76f19dc602
commit b00d4e55ee
2 changed files with 242 additions and 15 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from pathlib import Path
import os
import subprocess
import sys
import tempfile
@@ -30,7 +31,12 @@ from govoplan_release.release_execution import ( # noqa: E402
verify_repository_preflight_binding,
_clone_catalog_sources,
_checkout_frozen_source,
_flatpak_proxy_environment,
_run_isolated_wheel_builder,
_trusted_flatpak_builder_proxy,
_trusted_host_bubblewrap,
_verify_exact_publication_delta,
isolated_builder_launcher,
)
from govoplan_release.selective_catalog import ( # noqa: E402
enforce_frozen_selected_source_provenance,
@@ -39,6 +45,91 @@ from govoplan_release.selective_catalog import ( # noqa: E402
class ReleaseExecutionTests(unittest.TestCase):
def test_flatpak_proxy_environment_drops_unrelated_values(self) -> None:
with patch.dict(
os.environ,
{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/attacker/bus",
"XDG_RUNTIME_DIR": "/run/user/1000",
"PYTHONPATH": "/attacker",
"GIT_DIR": "/attacker/repository",
},
clear=True,
):
environment = _flatpak_proxy_environment()
self.assertEqual("/usr/bin:/bin", environment["PATH"])
self.assertEqual(
"unix:path=/run/flatpak/bus",
environment["DBUS_SESSION_BUS_ADDRESS"],
)
self.assertNotIn("XDG_RUNTIME_DIR", environment)
self.assertNotIn("PYTHONPATH", environment)
self.assertNotIn("GIT_DIR", environment)
def test_host_bubblewrap_preflight_requires_root_owned_safe_binary(self) -> None:
with patch(
"govoplan_release.release_execution.subprocess.run",
return_value=SimpleNamespace(
returncode=0,
stdout="81ed|0\n",
),
):
self.assertTrue(_trusted_host_bubblewrap())
with patch(
"govoplan_release.release_execution.subprocess.run",
return_value=SimpleNamespace(
returncode=0,
stdout="81ff|1000\n",
),
):
self.assertFalse(_trusted_host_bubblewrap())
def test_builder_launcher_uses_only_preflighted_flatpak_proxy(self) -> None:
with patch(
"govoplan_release.release_execution._trusted_flatpak_builder_proxy",
return_value=True,
):
self.assertEqual(
("/usr/bin/flatpak-spawn", "--host", "/usr/bin/bwrap"),
isolated_builder_launcher(),
)
def test_flatpak_proxy_requires_recognized_container(self) -> None:
with patch.dict(os.environ, {}, clear=True):
self.assertFalse(_trusted_flatpak_builder_proxy())
def test_flatpak_builder_launch_uses_sanitized_environment(self) -> None:
process = SimpleNamespace(wait=lambda timeout: 0)
command = (
"/usr/bin/flatpak-spawn",
"--host",
"/usr/bin/bwrap",
)
with (
patch.dict(
os.environ,
{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/attacker/bus",
"PYTHONPATH": "/attacker",
},
clear=True,
),
patch(
"govoplan_release.release_execution.subprocess.Popen",
return_value=process,
) as popen,
):
self.assertEqual(0, _run_isolated_wheel_builder(command))
environment = popen.call_args.kwargs["env"]
self.assertEqual(
"unix:path=/run/flatpak/bus",
environment["DBUS_SESSION_BUS_ADDRESS"],
)
self.assertNotIn("PYTHONPATH", environment)
def test_git_environment_ignores_inherited_redirection_and_commands(self) -> None:
with patch.dict(
"os.environ",
@@ -510,6 +601,11 @@ class ReleaseExecutionTests(unittest.TestCase):
'[project]\nname = "govoplan-files"\nversion = "1.2.3"\n',
encoding="utf-8",
)
_git(workspace, "init", "-b", "main", str(python_repo))
_git(python_repo, "config", "user.name", "Release Test")
_git(python_repo, "config", "user.email", "release@example.test")
_git(python_repo, "add", "pyproject.toml")
_git(python_repo, "commit", "-m", "Source")
specs = (
RepositorySpec(
name="govoplan-files",
@@ -530,6 +626,12 @@ class ReleaseExecutionTests(unittest.TestCase):
def build(command: tuple[str, ...]) -> int:
wheel_dir = Path(command[command.index("--bind") + 1])
second_bind = command.index("--bind", command.index("--bind") + 1)
source_dir = Path(command[second_bind + 1])
(source_dir / "build-mutated.txt").write_text(
"isolated copy",
encoding="utf-8",
)
(
wheel_dir / "govoplan_files-1.2.3-py3-none-any.whl"
).write_bytes(b"wheel")
@@ -581,10 +683,18 @@ class ReleaseExecutionTests(unittest.TestCase):
},
)
self.assertFalse((python_repo / "build-mutated.txt").exists())
self.assertEqual({"govoplan-files"}, set(artifacts))
self.assertEqual(1, runner.call_count)
command = runner.call_args.args[0]
self.assertIn(str(python_repo), command)
self.assertNotIn(str(python_repo), command)
self.assertTrue(
any(
value.startswith(str(candidate / "artifacts"))
and value.endswith("/source")
for value in command
)
)
self.assertIn("--unshare-all", command)
self.assertIn("PIP_NO_INDEX", command)
self.assertNotIn(str(Path.home()), command)