Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/composer-lock-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/playground-journeys.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/sdk-children.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions polyglot/laravel/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

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

Check warning on line 16 in polyglot/laravel/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this RUN instruction with the consecutive ones.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaHTCcez8IZ3BlS99wh4&open=AaHTCcez8IZ3BlS99wh4&pullRequest=337
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 \
Expand Down
3 changes: 3 additions & 0 deletions polyglot/php_worker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
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

Check warning on line 11 in polyglot/php_worker/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this RUN instruction with the consecutive ones.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaHTCcb58IZ3BlS99wh3&open=AaHTCcb58IZ3BlS99wh3&pullRequest=337
RUN apt-get update && apt-get install -y \
libcurl4-openssl-dev unzip \
&& docker-php-ext-install curl pcntl \
Expand Down
3 changes: 3 additions & 0 deletions polyglot/rust_worker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
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

Check warning on line 30 in polyglot/rust_worker/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this RUN instruction with the consecutive ones.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaHTCcWQ8IZ3BlS99wh2&open=AaHTCcWQ8IZ3BlS99wh2&pullRequest=337
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
Expand Down
113 changes: 113 additions & 0 deletions tests/Unit/AptNoRetriesContractTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Yaml\Yaml;

/**
* 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. Every networked APT path runs with retries off, or behind the central
* fixed-Bookworm-APT guard on a pinned parent.
*/
final class AptNoRetriesContractTest extends TestCase
{
private const NO_RETRIES = 'Acquire::Retries "0";';

private const APT = '/\bapt(?:-get)?\b[^\n]{0,80}\b(?:update|install|upgrade|dist-upgrade)\b|--with-deps|install-deps/';

public function test_every_apt_capable_workflow_job_disables_retries_first(): void
{
$unprotected = [];
foreach (glob($this->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<array<string, mixed>> $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);
}
}
Loading