diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b984645..2e03ffb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,9 @@ jobs: with: clients: pip,python,composer,curl,git,npm,cargo + - name: Disable APT download retries (CVE-2026-109518) + run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 with: php-version: '8.3' diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index fe24358..d6a06f2 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -30,6 +30,8 @@ jobs: - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust with: clients: pip,python,composer,curl,git,npm,cargo + - name: Disable APT download retries (CVE-2026-109518) + run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 with: php-version: '8.3' diff --git a/tests/test_apt_no_retries.py b/tests/test_apt_no_retries.py new file mode 100644 index 0000000..99c0290 --- /dev/null +++ b/tests/test_apt_no_retries.py @@ -0,0 +1,49 @@ +"""Every networked APT path in a workflow job runs with APT retries off. + +CVE-2026-109518 (durable-workflow/.github#295): vulnerable APT can follow a +redirect on a retried package download and install a swapped package as root. +Writing Acquire::Retries "0" before the first APT-capable step removes that +precondition. shivammathur/setup-php can run apt-get itself, so it counts. +""" + +from pathlib import Path +import re +import unittest + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOWS = ROOT / '.github/workflows' +NO_RETRIES = "echo 'Acquire::Retries \"0\";' | sudo tee /etc/apt/apt.conf.d/99-no-retries" +APT = re.compile(r'\bapt(?:-get)?\b[^\n]{0,80}\b(?:update|install|upgrade|dist-upgrade)\b|--with-deps|install-deps') + + +def apt_capable(step): + return str(step.get('uses', '')).startswith('shivammathur/setup-php@') or bool(APT.search(str(step.get('run') or ''))) + + +def unprotected(document): + for name, job in (document.get('jobs') or {}).items(): + for step in job.get('steps') or []: + if NO_RETRIES in str(step.get('run') or ''): + break + if apt_capable(step): + yield name, step.get('name') or step.get('uses') or step.get('run') + break + + +class AptNoRetriesTest(unittest.TestCase): + def test_every_apt_capable_job_disables_retries_first(self): + found = [] + for path in sorted(WORKFLOWS.glob('*.y*ml')): + for job, step in unprotected(yaml.safe_load(path.read_text())): + found.append(f'{path.name}:{job}: {step}') + self.assertEqual([], found) + + def test_checker_refuses_a_late_or_missing_line(self): + setup = {'uses': 'shivammathur/setup-php@' + 'a' * 40} + line = {'run': NO_RETRIES + ' >/dev/null'} + self.assertEqual([], list(unprotected({'jobs': {'ok': {'steps': [line, setup]}}}))) + self.assertEqual(1, len(list(unprotected({'jobs': {'late': {'steps': [setup, line]}}})))) + self.assertEqual(1, len(list(unprotected({'jobs': {'none': {'steps': [{'run': 'sudo apt-get update'}]}}})))) + self.assertEqual(1, len(list(unprotected({'jobs': {'deps': {'steps': [{'run': 'npx playwright install --with-deps chromium'}]}}})))) diff --git a/tests/test_ci_trust.py b/tests/test_ci_trust.py index c7e6eaf..240dac1 100644 --- a/tests/test_ci_trust.py +++ b/tests/test_ci_trust.py @@ -19,7 +19,7 @@ test_php_linux_qualification, test_php_linux_qualification_workflow, test_bookworm_apt_source, test_private_pip_runtime, test_php_linux_normalize, test_php_linux_archive_metadata, test_php_linux_alternatives, - test_hosted_runtime_parent) + test_hosted_runtime_parent, test_apt_no_retries) ROOT = Path(__file__).resolve().parents[1] @@ -39,7 +39,8 @@ def load_tests(loader, standard_tests, pattern): test_php_linux_recipe, test_php_linux_archive, test_setup_php_linux, test_php_linux_qualification, test_php_linux_qualification_workflow, test_bookworm_apt_source, test_private_pip_runtime, test_hosted_runtime_parent, - test_php_linux_normalize, test_php_linux_archive_metadata, test_php_linux_alternatives): + test_php_linux_normalize, test_php_linux_archive_metadata, test_php_linux_alternatives, + test_apt_no_retries): standard_tests.addTests(loader.loadTestsFromModule(module)) return standard_tests