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
2 changes: 2 additions & 0 deletions .github/tracing-performance/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
FROM php:8.3.35-cli-bookworm
# CVE-2026-109518 (durable-workflow/.github#295): no APT download retries.
RUN echo 'Acquire::Retries "0";' > /etc/apt/apt.conf.d/99-no-retries

Check warning on line 3 in .github/tracing-performance/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_workflow&issues=AaHTIfG3xgIGZ2MAOe7q&open=AaHTIfG3xgIGZ2MAOe7q&pullRequest=1128
RUN apt-get update && apt-get install -y --no-install-recommends \
git libonig-dev libxml2-dev unzip \
&& docker-php-ext-install pdo_mysql mbstring dom pcntl \
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/dependency-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
with:
persist-credentials: false

- 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.4"
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ jobs:
- name: Verify build and feature selection contracts
run: python scripts/ci/test-build-routing.py

- name: Require APT retries off before networked APT (CVE-2026-109518)
run: python -I scripts/ci/test-apt-no-retries.py

- name: Verify complete feature inventories
run: php scripts/ci/verify-feature-shards.php

Expand Down Expand Up @@ -315,6 +318,8 @@ jobs:
with:
persist-credentials: false

- 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: ${{ matrix.php }}
Expand Down Expand Up @@ -349,6 +354,8 @@ jobs:
with:
persist-credentials: false

- 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 the optional application instrumentation
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
Expand Down Expand Up @@ -975,6 +982,8 @@ jobs:
fetch-depth: 10
persist-credentials: false

- 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 coverage driver
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
Expand Down Expand Up @@ -1425,6 +1434,8 @@ jobs:
persist-credentials: false
ref: ${{ env.COVERAGE_COMMIT }}

- 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 coverage driver
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
Expand Down Expand Up @@ -1538,6 +1549,8 @@ jobs:
persist-credentials: false
ref: ${{ env.COVERAGE_COMMIT }}

- 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 for coverage aggregation
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ jobs:
with:
persist-credentials: false

- 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: ${{ matrix.php }}
Expand Down
2 changes: 2 additions & 0 deletions examples/embedded-http/runtime/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ RUN docker-php-ext-install -j1 bcmath opcache pcntl \
&& mkdir -p /app \
&& chown 1000:1000 /app
RUN docker-php-ext-install -j1 sockets
# CVE-2026-109518 (durable-workflow/.github#295): no APT download retries.
RUN echo 'Acquire::Retries "0";' > /etc/apt/apt.conf.d/99-no-retries
RUN apt-get update \
&& apt-get install -y --no-install-recommends unzip \
&& rm -rf /var/lib/apt/lists/*
Expand Down
81 changes: 81 additions & 0 deletions scripts/ci/test-apt-no-retries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Every networked APT path in a workflow job runs with APT retries off.

CVE-2026-109518 (durable-workflow/.github issue 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.
Standard library only: jobs are the two-space keys under the top-level jobs map.
"""

from pathlib import Path
import re
import unittest

ROOT = Path(__file__).resolve().parents[2]
WORKFLOWS = ROOT / '.github/workflows'
NO_RETRIES = "echo 'Acquire::Retries \"0\";' | sudo tee /etc/apt/apt.conf.d/99-no-retries"
APT = re.compile(r'shivammathur/setup-php@|\bapt(?:-get)?\b[^\n]{0,80}\b(?:update|install|upgrade|dist-upgrade)\b|--with-deps|install-deps')


def jobs(source):
body = re.split(r'(?m)^jobs:\s*$', source, maxsplit=1)[1]
body = re.split(r'(?m)^\S', body, maxsplit=1)[0]
parts = re.split(r'(?m)^ ([A-Za-z0-9_-]+):\s*$', body)

Check warning on line 24 in scripts/ci/test-apt-no-retries.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace spaces with quantifier `{2}`.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_workflow&issues=AaHTIe8uxgIGZ2MAOe7p&open=AaHTIe8uxgIGZ2MAOe7p&pullRequest=1128
return dict(zip(parts[1::2], parts[2::2]))


def unprotected(source):
for name, job in jobs(source).items():
job = re.sub(r'(?m)^\s*#.*$', '', job)
first = APT.search(job)
if first and (NO_RETRIES not in job or job.index(NO_RETRIES) > first.start()):
yield name


DOCKER_NO_RETRIES = re.compile(r"""^RUN echo 'Acquire::Retries "0";' > /etc/apt/apt.conf.d/99-no-retries$""", re.M)
DOCKER_APT = re.compile(r'^RUN\b[^\n]*\bapt(?:-get)?\b[^\n]{0,80}\b(?:update|install|upgrade|dist-upgrade)\b', re.M)


def unprotected_dockerfile(source):
"""Each build stage (FROM) disables retries before its first networked APT."""
for stage in re.split(r'(?m)^FROM\b', source)[1:]:
first = DOCKER_APT.search(stage)
guard = DOCKER_NO_RETRIES.search(stage)
if first and (guard is None or guard.start() > first.start()):
return True
return False


class AptNoRetriesTest(unittest.TestCase):
def test_every_dockerfile_disables_retries_before_apt(self):
found = [str(path.relative_to(ROOT)) for path in sorted(ROOT.rglob('Dockerfile*'))
if 'vendor' not in path.parts and 'node_modules' not in path.parts
and unprotected_dockerfile(path.read_text())]
self.assertEqual([], found)

def test_dockerfile_checker_refuses_a_late_or_missing_line(self):
guard = "RUN echo 'Acquire::Retries \"0\";' > /etc/apt/apt.conf.d/99-no-retries\n"
apt = "RUN apt-get update && apt-get install -y unzip\n"
self.assertFalse(unprotected_dockerfile("FROM php\n" + guard + apt))
self.assertTrue(unprotected_dockerfile("FROM php\n" + apt + guard))
self.assertTrue(unprotected_dockerfile("FROM php\n" + apt))
self.assertTrue(unprotected_dockerfile("FROM a\n" + guard + "FROM b\n" + apt))

def test_every_apt_capable_job_disables_retries_first(self):
found = [f'{path.name}:{job}' for path in sorted(WORKFLOWS.glob('*.y*ml'))
for job in unprotected(path.read_text())]
self.assertEqual([], found)

def test_checker_refuses_a_late_or_missing_line(self):
line = f" - run: {NO_RETRIES} >/dev/null\n"
setup = " - uses: shivammathur/setup-php@" + "a" * 40 + "\n"
workflow = "on: push\njobs:\n {}:\n steps:\n{}"
self.assertEqual([], list(unprotected(workflow.format('ok', line + setup))))
self.assertEqual(['late'], list(unprotected(workflow.format('late', setup + line))))
self.assertEqual(['none'], list(unprotected(workflow.format('none', " - run: sudo apt-get update\n"))))
self.assertEqual(['deps'], list(unprotected(workflow.format('deps', " - run: npx playwright install --with-deps chromium\n"))))


if __name__ == '__main__':
unittest.main()
Loading