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
3 changes: 2 additions & 1 deletion .github/workflows/phpunit-feature.yml
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ jobs:
docker build --target base -t durable-workflow-server-phpunit-base .
docker build -t durable-workflow-server-corpus-validator - <<'DOCKERFILE'
FROM durable-workflow-server-phpunit-base
RUN apt-get update \
RUN /usr/bin/env -i PATH=/usr/bin:/bin LC_ALL=C /bin/sh /usr/local/lib/server-apt/require-fixed-apt.sh \
&& apt-get update \
&& apt-get install -y --no-install-recommends git jq \
&& rm -rf /var/lib/apt/lists/* \
&& server-verify-trust final
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/public-boundary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ jobs:
run: |
python3 scripts/ci/check-docker-workflow-isolation.py
python3 -m unittest tests/Unit/Support/docker_workflow_isolation_policy_test.py
python3 -m unittest tests/Unit/Support/apt_no_retries_test.py
5 changes: 4 additions & 1 deletion scripts/conformance/worker-versioning-mixed.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ ARG PYTHON_SDK_VERSION
COPY bootstrap-patched-pip.sh blocked_roots.py /opt/dw-ci/
# apt checks package signatures rather than TLS trust. It refreshes the OS CA
# store, which must then exclude the withdrawn roots before any TLS download.
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates php-cli php-curl unzip \
# CVE-2026-109518 (durable-workflow/.github#295): the unpinned parent's APT may
# follow a redirect on a retried download, so retries are off first.
RUN printf 'Acquire::Retries "0";\n' > /etc/apt/apt.conf.d/99-no-retries \
&& apt-get update && apt-get install -y --no-install-recommends ca-certificates php-cli php-curl unzip \
&& rm -r /var/lib/apt/lists/* \
&& python3 /opt/dw-ci/blocked_roots.py /etc/ssl/certs/ca-certificates.crt
COPY --from=composer /usr/bin/composer /usr/bin/composer
Expand Down
5 changes: 4 additions & 1 deletion scripts/perf/Dockerfile.fpm-experiment
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ FROM ${SERVER_IMAGE} AS published

FROM php:8.3.35-fpm-trixie@sha256:ea1c5007c904d2cdf12e4ac87528c9f12407fb8d7636a31a93b4a862b22e2f8f

RUN apt-get update \
# CVE-2026-109518 (durable-workflow/.github#295): the parent's APT may follow a
# redirect on a retried download, so retries are off before any APT network call.
RUN printf 'Acquire::Retries "0";\n' > /etc/apt/apt.conf.d/99-no-retries \
&& apt-get update \
&& apt-get install -y --no-install-recommends libpq5 libzip5 \
&& groupmod --gid 1000 www-data \
&& usermod --uid 1000 --gid 1000 www-data \
Expand Down
64 changes: 64 additions & 0 deletions tests/Unit/Support/apt_no_retries_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Every networked APT path in a tracked Dockerfile, or in a Dockerfile inlined
in a workflow, runs on a proved fixed APT or with APT retries off.

CVE-2026-109518 (durable-workflow/.github#295): a vulnerable APT can follow a
redirect on a retried package download and install a swapped package as root.
In each build stage, the first networked APT command must come after either
the reviewed fixed-floor proof (docker/apt/require-fixed-apt.sh) or a line
that writes Acquire::Retries "0", which removes the retry precondition.
"""

from pathlib import Path
import re
import subprocess
import unittest

ROOT = Path(__file__).resolve().parents[3]
APT = re.compile(r'\bapt(?:-get)?\s+(?:-\S+\s+)*(?:update|install|upgrade|dist-upgrade|full-upgrade)\b')
GUARDS = ('require-fixed-apt.sh', 'Acquire::Retries "0"')
STAGE = re.compile(r'^\s*FROM\s', re.IGNORECASE | re.MULTILINE)


def dockerfiles():
tracked = subprocess.run(['git', 'ls-files'], cwd=ROOT, capture_output=True, text=True, check=True).stdout.split()
for path in tracked:
name = path.rsplit('/', 1)[-1].lower()
inline = path.startswith('.github/workflows/') and name.endswith(('.yml', '.yaml'))
if name.startswith('dockerfile') or name.endswith('.dockerfile') or inline:
yield path


def unprotected(text):
"""Stages whose first networked APT command has no guard before it."""
for index, stage in enumerate(STAGE.split(text)):
found = APT.search(stage)
if found is None:
continue
before = stage[:found.start()]
if not any(guard in before for guard in GUARDS):
yield index, found.group(0)


class AptNoRetriesTest(unittest.TestCase):
def test_every_networked_apt_stage_is_guarded_first(self):
found = [f'{path}: stage {stage}: {command}'
for path in dockerfiles()
for stage, command in unprotected((ROOT / path).read_text(encoding='utf-8'))]
self.assertEqual([], found)

def test_checker_refuses_a_late_or_missing_guard(self):
retries = "printf 'Acquire::Retries \"0\";\\n' > /etc/apt/apt.conf.d/99-no-retries"
self.assertEqual([], list(unprotected(f'FROM a\nRUN {retries} \\\n && apt-get update\n')))
self.assertEqual([], list(unprotected('FROM a\nRUN sh require-fixed-apt.sh && apt-get install -y x\n')))
self.assertEqual(1, len(list(unprotected(f'FROM a\nRUN apt-get update && {retries}\n'))))
self.assertEqual(1, len(list(unprotected('FROM a\nRUN apt-get install -y --no-install-recommends x\n'))))
# A Dockerfile inlined in a workflow step is checked from its own FROM.
inline = 'run: |\n docker build - <<EOF\n FROM base\n RUN apt-get update\n EOF\n'
self.assertEqual(1, len(list(unprotected(inline))))
# A guard in one stage does not cover the next stage.
self.assertEqual(1, len(list(unprotected(f'FROM a\nRUN {retries} && apt-get update\nFROM b\nRUN apt upgrade\n'))))


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