diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2101a272..afa916c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,9 @@ jobs: with: clients: pip,python,composer,curl,git,npm + - 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 + - name: Set up PHP uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0 with: @@ -163,6 +166,9 @@ jobs: with: clients: pip,python,composer,curl,git + - 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 + - name: Set up PHP uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0 with: diff --git a/.github/workflows/composer-lock-audit.yml b/.github/workflows/composer-lock-audit.yml index 81ccc57b..7acd9dfa 100644 --- a/.github/workflows/composer-lock-audit.yml +++ b/.github/workflows/composer-lock-audit.yml @@ -48,6 +48,9 @@ jobs: with: clients: pip,python,composer,curl,git,npm + - 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 + - name: Set up PHP uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0 with: diff --git a/.github/workflows/playground-journeys.yml b/.github/workflows/playground-journeys.yml index 1918df5f..70fe348b 100644 --- a/.github/workflows/playground-journeys.yml +++ b/.github/workflows/playground-journeys.yml @@ -34,6 +34,9 @@ jobs: with: clients: pip,python,composer,curl,git + - 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 + - name: Set up PHP uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0 with: diff --git a/.github/workflows/sdk-children.yml b/.github/workflows/sdk-children.yml index f2e3597e..e5ba9f16 100644 --- a/.github/workflows/sdk-children.yml +++ b/.github/workflows/sdk-children.yml @@ -37,6 +37,8 @@ jobs: with: node-version: 24 cache: npm + - 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 - name: Install locked browser tooling run: | npm ci --ignore-scripts diff --git a/polyglot/laravel/Dockerfile b/polyglot/laravel/Dockerfile index 13a51a80..894812c4 100644 --- a/polyglot/laravel/Dockerfile +++ b/polyglot/laravel/Dockerfile @@ -11,6 +11,9 @@ ARG DURABLE_WORKFLOW_WATERLINE_VERSION COPY docker/install-phpredis.sh /usr/local/bin/install-phpredis +# No APT download retries before the first networked apt-get: CVE-2026-109518 +# (durable-workflow/.github#295). +RUN echo 'Acquire::Retries "0";' > /etc/apt/apt.conf.d/99-no-retries RUN apt-get update && apt-get install -y \ curl libpq-dev libzip-dev unzip git \ && docker-php-ext-install pdo pdo_mysql pcntl zip bcmath \ diff --git a/polyglot/php_worker/Dockerfile b/polyglot/php_worker/Dockerfile index 2baed693..cbf6c338 100644 --- a/polyglot/php_worker/Dockerfile +++ b/polyglot/php_worker/Dockerfile @@ -6,6 +6,9 @@ FROM php:8.4-cli ARG DURABLE_WORKFLOW_PHP_SDK_PIN= ARG DURABLE_WORKFLOW_PHP_SDK_VERSION +# No APT download retries before the first networked apt-get: CVE-2026-109518 +# (durable-workflow/.github#295). +RUN echo 'Acquire::Retries "0";' > /etc/apt/apt.conf.d/99-no-retries RUN apt-get update && apt-get install -y \ libcurl4-openssl-dev unzip \ && docker-php-ext-install curl pcntl \ diff --git a/polyglot/rust_worker/Dockerfile b/polyglot/rust_worker/Dockerfile index ff920362..5ad0ac55 100644 --- a/polyglot/rust_worker/Dockerfile +++ b/polyglot/rust_worker/Dockerfile @@ -25,6 +25,9 @@ ARG APACHE_AVRO_RUST_VERSION=0.21.0 ENV DURABLE_WORKFLOW_RUST_SDK_VERSION=${DURABLE_WORKFLOW_RUST_SDK_VERSION} \ APACHE_AVRO_RUST_VERSION=${APACHE_AVRO_RUST_VERSION} +# No APT download retries before the first networked apt-get: CVE-2026-109518 +# (durable-workflow/.github#295). +RUN echo 'Acquire::Retries "0";' > /etc/apt/apt.conf.d/99-no-retries RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* diff --git a/tests/Unit/AptNoRetriesContractTest.php b/tests/Unit/AptNoRetriesContractTest.php new file mode 100644 index 00000000..27c6b08f --- /dev/null +++ b/tests/Unit/AptNoRetriesContractTest.php @@ -0,0 +1,113 @@ +path('.github/workflows').'/*.yml') as $file) { + $workflow = Yaml::parseFile($file); + foreach ($workflow['jobs'] ?? [] as $name => $job) { + $finding = $this->firstUnprotectedStep($job['steps'] ?? []); + if ($finding !== null) { + $unprotected[] = basename($file).':'.$name.': '.$finding; + } + } + } + + $this->assertSame([], $unprotected); + } + + public function test_the_workflow_check_refuses_a_late_or_missing_line(): void + { + $setup = ['uses' => 'shivammathur/setup-php@'.str_repeat('a', 40)]; + $line = ['run' => "echo '".self::NO_RETRIES."' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null"]; + + $this->assertNull($this->firstUnprotectedStep([$line, $setup])); + $this->assertNotNull($this->firstUnprotectedStep([$setup, $line])); + $this->assertNotNull($this->firstUnprotectedStep([['run' => 'npx playwright install --with-deps chromium']])); + } + + public function test_every_image_stage_disables_retries_or_runs_the_fixed_apt_guard_before_apt(): void + { + $unprotected = []; + foreach (['Dockerfile', '.devcontainer/docker/Dockerfile', ...glob($this->path('polyglot').'/*/Dockerfile')] as $file) { + $path = str_starts_with($file, '/') ? $file : $this->path($file); + $stages = preg_split('/^FROM /m', (string) file_get_contents($path)); + foreach (array_slice($stages, 1) as $index => $stage) { + if ($this->stageRunsAptUnprotected($stage)) { + $unprotected[] = str_replace($this->path(''), '', $path).' stage '.($index + 1); + } + } + } + + $this->assertSame([], $unprotected); + } + + public function test_the_image_check_refuses_apt_before_the_line(): void + { + $this->assertTrue($this->stageRunsAptUnprotected("debian:bookworm-slim\nRUN apt-get update\n")); + $this->assertTrue($this->stageRunsAptUnprotected( + "debian:bookworm-slim\nRUN apt-get update\nRUN echo '".self::NO_RETRIES."' > /etc/apt/apt.conf.d/99-no-retries\n", + )); + $this->assertFalse($this->stageRunsAptUnprotected( + "debian:bookworm-slim\nRUN echo '".self::NO_RETRIES."' > /etc/apt/apt.conf.d/99-no-retries\nRUN apt-get update\n", + )); + } + + /** @param list> $steps */ + private function firstUnprotectedStep(array $steps): ?string + { + foreach ($steps as $step) { + $run = (string) ($step['run'] ?? ''); + if (str_contains($run, self::NO_RETRIES)) { + return null; + } + if (str_starts_with((string) ($step['uses'] ?? ''), 'shivammathur/setup-php@') || preg_match(self::APT, $run) === 1) { + return (string) ($step['name'] ?? $step['uses'] ?? $run); + } + } + + return null; + } + + private function stageRunsAptUnprotected(string $stage): bool + { + $stage = (string) preg_replace('/^\s*#.*$/m', '', $stage); + $apt = preg_match('/apt-get (?:update|install|upgrade)|--with-deps|\bsh \S*install-vendor\.sh/', $stage, $match, PREG_OFFSET_CAPTURE); + if ($apt !== 1) { + return false; + } + $first = $match[0][1]; + foreach ([self::NO_RETRIES, 'require-fixed-bookworm-apt.sh \\', 'require-fixed-bookworm-apt.sh;'] as $protection) { + $offset = strpos($stage, $protection); + if ($offset !== false && $offset < $first) { + return false; + } + } + + return true; + } + + private function path(string $relative): string + { + return dirname(__DIR__, 2).($relative === '' ? '/' : '/'.$relative); + } +}