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
32 changes: 32 additions & 0 deletions .github/workflows/php73-sury.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Validate PHP 7.3 Sury backport

on:
pull_request:
paths:
- php-apache/7.3/**
- .github/workflows/php73-sury.yml
workflow_dispatch:

permissions:
contents: read

jobs:
compatibility:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
- name: Build patched image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8
with:
context: ./php-apache/7.3
target: php-apache
platforms: linux/amd64
load: true
tags: devitools/php-apache:7.3
cache-from: type=gha,scope=php73-compatibility
cache-to: type=gha,mode=max,scope=php73-compatibility
- name: Verify compatibility and security behavior
run: python3 php-apache/7.3/.scripts/smoke.py devitools/php-apache:7.3
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ $ sudo ln -s $(pwd)/build.sh /usr/bin/dbr
```shell
$ dbr vue 3.0.0
```

## PHP 7.3 com Apache

[`devitools/php-apache:7.3`](php-apache/7.3/README.md): substituta de compatibilidade para `webdevops/php-apache:7.3`, com backports do pacote-fonte Surý para CVE-2026-109618 e CVE-2026-109619. Consulte as instruções de build, validação e as limitações de suporte antes de migrar.
33 changes: 33 additions & 0 deletions php-apache/7.3/.scripts/build-php.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
set -euxo pipefail

php -r '$m = get_loaded_extensions(); sort($m); file_put_contents("/tmp/upstream-modules.json", json_encode($m));'
php-config --configure-options > /tmp/configure-options
sed -i 's|http://deb.debian.org/debian|http://archive.debian.org/debian|g; s|http://security.debian.org/debian-security|http://archive.debian.org/debian-security|g' /etc/apt/sources.list
apt-get -o Acquire::Check-Valid-Until=false update
apt-get install -y --no-install-recommends $PHPIZE_DEPS patch \
libargon2-dev libcurl4-openssl-dev libedit-dev libonig-dev \
libreadline-dev libsodium-dev libsqlite3-dev libssl-dev libxml2-dev zlib1g-dev

mkdir -p /security /tmp/sury /usr/src/php /patched
curl -fsSL "https://packages.sury.org/php/pool/main/p/php7.3/php7.3_${SURY_SOURCE_VERSION}.debian.tar.xz" -o /tmp/sury.tar.xz
printf '%s %s\n' "$SURY_SOURCE_SHA256" /tmp/sury.tar.xz | sha256sum -c -
tar -xJf /tmp/sury.tar.xz -C /tmp/sury
printf '%s %s\n' 166eaccde933381da9516a2b70ad0f447d7cec4b603d07b9a916032b215b90cc /usr/src/php.tar.xz | sha256sum -c -
tar -xJf /usr/src/php.tar.xz -C /usr/src/php --strip-components=1
cd /usr/src/php
for name in 0079-Introduce-max_multipart_body_parts-INI.patch 0107-backport-ZSTR_MAX_LEN.patch 0139-Avoid-state-corruption-via-bailout.patch; do
patch --batch --fuzz=0 -p1 < "/tmp/sury/debian/patches/$name"
cp "/tmp/sury/debian/patches/$name" /security/
done
cp /tmp/sury/debian/changelog /security/sury-changelog
cp /tmp/upstream-modules.json /security/upstream-modules.json
printf '%s\n' "$SURY_SOURCE_VERSION" > /security/source-version
printf '%s\n' "$SURY_SOURCE_SHA256" > /security/source-sha256
export CFLAGS="$PHP_CFLAGS" CPPFLAGS="$PHP_CPPFLAGS" LDFLAGS="$PHP_LDFLAGS"
read -r -a configure_options < /tmp/configure-options
./configure "${configure_options[@]}" --enable-opcache=shared
make -j"$BUILD_JOBS"
make install INSTALL_ROOT=/patched
rm -rf /patched/usr/local/etc /patched/usr/local/php /patched/usr/local/var
cp /tmp/configure-options /security/upstream-configure-options
/patched/usr/local/bin/php -n -r 'exit(PHP_VERSION === "7.3.33" ? 0 : 1);'
125 changes: 125 additions & 0 deletions php-apache/7.3/.scripts/smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import json
import pathlib
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request

image = sys.argv[1] if len(sys.argv) > 1 else "devitools/php-apache:7.3"


def docker(*args, check=True):
return subprocess.run(["docker", *args], check=check, text=True, capture_output=True)


def require(condition, message):
if not condition:
raise RuntimeError(message)


with tempfile.TemporaryDirectory(prefix="php73-sury-") as directory:
root = pathlib.Path(directory)
app = root / "app"
app.mkdir()
app.joinpath("index.php").write_text('''<?php
header('Content-Type: application/json');
$db = new PDO('sqlite::memory:');
$im = imagecreatetruecolor(2, 2);
ob_start();
imagepng($im);
$png = ob_get_clean();
$modules = get_loaded_extensions();
sort($modules);
echo json_encode([
'version' => PHP_VERSION,
'sapi' => PHP_SAPI,
'memory_limit' => ini_get('memory_limit'),
'mounted' => ini_get('precision'),
'upload_progress' => ini_get('session.upload_progress.enabled'),
'multipart_limit' => ini_get('max_multipart_body_parts'),
'sqlite' => $db->query('SELECT 42')->fetchColumn(),
'gd' => strlen($png) > 0,
'mbstring' => mb_strlen('á', 'UTF-8'),
'modules' => $modules,
'post' => $_POST,
'files' => count($_FILES),
]);
''')
app.joinpath("oom.php").write_text('''<?php
register_shutdown_function(function () { file_put_contents('/tmp/unsafe-shutdown', 'ran'); });
ini_set('memory_limit', '16M');
$x = str_repeat('x', 64 * 1024 * 1024);
''')
custom = root / "custom.ini"
custom.write_text("precision=12\n")
cid = docker(
"run", "-d", "--platform", "linux/amd64", "-p", "127.0.0.1::80",
"-e", "WEB_DOCUMENT_ROOT=/app/public", "-e", "PHP_MEMORY_LIMIT=96M",
"-v", f"{app}:/app/public:ro",
"-v", f"{custom}:/opt/docker/etc/php/php.ini:ro", image,
).stdout.strip()
try:
port = docker("port", cid, "80/tcp").stdout.strip().split(":")[-1]
base = f"http://127.0.0.1:{port}"
for attempt in range(60):
try:
with urllib.request.urlopen(base, timeout=2) as response:
data = json.load(response)
break
except (OSError, ValueError):
time.sleep(1)
else:
raise RuntimeError("Apache/FPM did not become ready")
docker("exec", cid, "apache2ctl", "-t")
docker("exec", cid, "php-fpm", "-t")
require(data["version"] == "7.3.33", "Wrong PHP version")
require(data["sapi"] == "fpm-fcgi", "HTTP is not using FPM")
require(data["memory_limit"] == "96M", "PHP environment not applied")
require(data["mounted"] == "12", "Mounted INI not applied")
require(data["upload_progress"] in ("", "0", False), "Upload progress is enabled")
require(data["multipart_limit"] == "-1", "Missing multipart prerequisite")
require(int(data["sqlite"]) == 42 and data["gd"] and data["mbstring"] == 1, "Extension functional check failed")
expected = json.loads(docker("exec", cid, "cat", "/usr/local/share/php-security/upstream-modules.json").stdout)
require(set(data["modules"]) == set(expected) | {"cgi-fcgi"}, "HTTP extension parity failed: " + json.dumps({"expected": expected, "actual": data["modules"]}))
boundary = "php73-sury-smoke"
body = (
f"--{boundary}\r\nContent-Disposition: form-data; name=field\r\n\r\nvalue\r\n"
f"--{boundary}\r\nContent-Disposition: form-data; name=file; filename=test.txt\r\n"
f"Content-Type: text/plain\r\n\r\nsafe upload\r\n--{boundary}--\r\n"
).encode()
request = urllib.request.Request(base, data=body, headers={"Content-Type": f"multipart/form-data; boundary={boundary}"})
with urllib.request.urlopen(request, timeout=5) as response:
uploaded = json.load(response)
require(uploaded["post"] == {"field": "value"} and uploaded["files"] == 1, "Multipart upload failed")
try:
urllib.request.urlopen(base + "/oom.php", timeout=10).close()
except (OSError, urllib.error.HTTPError):
pass
require(docker("exec", cid, "test", "!", "-e", "/tmp/unsafe-shutdown", check=False).returncode == 0, "Unsafe OOM shutdown callback executed")
with urllib.request.urlopen(base, timeout=10) as response:
require(json.load(response)["version"] == "7.3.33", "FPM did not recover after OOM")
cli = docker("exec", cid, "php", "/usr/local/share/php-security/verify.php")
print(cli.stdout.strip())
except Exception:
print(docker("logs", cid, check=False).stdout, file=sys.stderr)
print(docker("logs", cid, check=False).stderr, file=sys.stderr)
raise
finally:
docker("rm", "-f", cid, check=False)

oom = docker(
"run", "--rm", "--platform", "linux/amd64", "--entrypoint", "php", image,
"-d", "memory_limit=16M", "-r",
"register_shutdown_function(function () { echo 'UNSAFE_SHUTDOWN'; }); str_repeat('x', 64 * 1024 * 1024);",
check=False,
)
require(oom.returncode == 1 and "Allowed memory size" in oom.stderr, "OOM did not follow the patched immediate-exit path")
require("UNSAFE_SHUTDOWN" not in oom.stdout, "Unsafe CLI OOM shutdown callback executed")

result = docker(
"run", "--rm", "--platform", "linux/amd64", "-e", "PHP_DISMOD=redis,imagick",
image, "php", "-r", "exit(extension_loaded('redis') || extension_loaded('imagick') ? 1 : 0);",
)
print("Apache/FPM HTTP, multipart, INI mounts, environment, extensions, OOM recovery and PHP_DISMOD passed")
17 changes: 17 additions & 0 deletions php-apache/7.3/.scripts/verify.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
$expected = json_decode(file_get_contents(__DIR__ . '/upstream-modules.json'), true);
$actual = get_loaded_extensions();
sort($actual);
if ($actual !== $expected) {
fwrite(STDERR, 'Extension mismatch: ' . json_encode(array_diff($expected, $actual)) . PHP_EOL);
exit(1);
}
if (PHP_VERSION !== '7.3.33' || PHP_ZTS || PHP_DEBUG || ini_get('session.upload_progress.enabled')) {
fwrite(STDERR, 'Unexpected PHP version, ABI or upload progress setting' . PHP_EOL);
exit(1);
}
if (ini_get('max_multipart_body_parts') === false) {
fwrite(STDERR, 'Missing multipart prerequisite patch' . PHP_EOL);
exit(1);
}
echo 'PHP version, extension parity and security configuration verified', PHP_EOL;
36 changes: 36 additions & 0 deletions php-apache/7.3/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
FROM webdevops/php-apache:7.3@sha256:aec98f365a8f9e35ff354ac1c0893ba15141807557f40f31f02fea6ed540c43b AS upstream

FROM upstream AS builder

ARG SURY_SOURCE_VERSION=7.3.33-35+0~20261021.144+debian12~1.gbpecf76f
ARG SURY_SOURCE_SHA256=6708d986af131aa0e0c7ecb65a47babacf2918d43a3d94d31341f64a734aa815
ARG BUILD_JOBS=2

COPY .scripts/build-php.sh /tmp/build-php.sh
RUN bash /tmp/build-php.sh

RUN test "$(php-config --extension-dir)" = "$(/patched/usr/local/bin/php-config --extension-dir)" \
&& mkdir -p /tmp/patched-source/php \
&& tar -xJf /usr/src/php.tar.xz -C /tmp/patched-source/php --strip-components=1 \
&& cd /tmp/patched-source/php \
&& patch --batch --fuzz=0 -p1 < /security/0079-Introduce-max_multipart_body_parts-INI.patch \
&& patch --batch --fuzz=0 -p1 < /security/0107-backport-ZSTR_MAX_LEN.patch \
&& patch --batch --fuzz=0 -p1 < /security/0139-Avoid-state-corruption-via-bailout.patch \
&& tar -cJf /tmp/php-patched.tar.xz -C /tmp/patched-source php

FROM upstream AS php-apache

COPY --from=builder /patched/usr/local/ /usr/local/
COPY --from=builder /tmp/php-patched.tar.xz /usr/src/php.tar.xz
COPY --from=builder /security/ /usr/local/share/php-security/
COPY .scripts/verify.php /usr/local/share/php-security/verify.php

RUN rm -f /usr/src/php.tar.xz.asc \
&& printf 'session.upload_progress.enabled=0\n' > /usr/local/etc/php/conf.d/zz-security.ini \
&& php /usr/local/share/php-security/verify.php \
&& php-fpm -t

LABEL org.opencontainers.image.title="PHP 7.3 Apache with Sury security backports" \
org.opencontainers.image.source="https://github.com/devitools/dockerfile" \
devitools.php.security-fixes="CVE-2026-109618,CVE-2026-109619" \
devitools.php.patch-source="Sury php7.3 7.3.33-35"
46 changes: 46 additions & 0 deletions php-apache/7.3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# PHP 7.3 Apache compatibility image

`devitools/php-apache:7.3` replaces the public `webdevops/php-apache:7.3` image for existing linux/amd64 deployments. The existing build-images workflow publishes this tag after merge to main. This directory deliberately does not define a `latest` alias.

## Security patch provenance

The original image is pinned to `sha256:aec98f365a8f9e35ff354ac1c0893ba15141807557f40f31f02fea6ed540c43b`. Its PHP is compiled under `/usr/local`, not managed by Debian packages. Installing Sury binary packages alongside it would leave the PHP used by FPM unchanged.

Instead, the builder downloads the checksum-pinned Debian source package `php7.3_7.3.33-35+0~20261021.144+debian12~1.gbpecf76f.debian.tar.xz` from [Sury](https://packages.sury.org/php/pool/main/p/php7.3/). It applies these unmodified patches to the checksum-pinned upstream PHP 7.3.33 source included in the original image:

- `0079-Introduce-max_multipart_body_parts-INI.patch`: the multipart limit prerequisite assumed by the later backport.
- `0107-backport-ZSTR_MAX_LEN.patch`: the string-length definitions required by the later backport.
- `0139-Avoid-state-corruption-via-bailout.patch`: Sury's backport for CVE-2026-109618 and CVE-2026-109619.

The build reuses the original configure options and compiler flags, rebuilds CLI, FPM, sodium and OPcache, and installs binaries and headers under the original paths. Existing additional extensions are inherited unchanged. The `/usr/src/php.tar.xz` archive is also replaced with the patched source for downstream `docker-php-ext-install` builds; the obsolete upstream archive signature is removed. Only these three patches are applied; this image does **not** contain every other fix from Sury revision 35 and does not install Sury binary `.deb` packages. PHP still reports 7.3.33, so `php -v` alone does not identify the fix and version-only scanners may continue to flag these CVEs. Use the recorded patch provenance and behavioral test results when assessing this image.

The source revision, checksum, patches, Sury changelog and original extension inventory are recorded in `/usr/local/share/php-security/`. Build tools and archived Debian repositories are confined to the builder stage.

`session.upload_progress.enabled=0` is set in `/usr/local/etc/php/conf.d/zz-security.ini`, following the [PHP advisory of October 23, 2026](https://www.php.net/archive/2026.php). Keep this setting disabled in custom configuration, including mounted INI files.

## Compatibility

The final stage inherits the original image, preserving:

- `/opt/docker` entrypoint, service scripts, Supervisor, Apache and FPM configuration.
- `/app`, the `application` user (UID/GID 1000), exposed ports and volume behavior.
- `WEB_DOCUMENT_ROOT`, `WEB_ALIAS_DOMAIN`, `PHP_*`, `PHP_DISMOD`, `php.*`, `fpm.pool.*`, `fpm.global.*`, and other original environment handling.
- Existing `/opt/docker/etc/php/php.ini`, `/usr/local/etc/php/conf.d`, Apache and FPM configuration mounts.
- The exact loaded extension names from the pinned original image, including ionCube, vips, amqp, redis, memcached, mongodb and imagick. The build fails if extension parity is lost.

Third-party extensions retain their original versions. Application-specific behavior, encoded ionCube applications and downstream images that compile additional extensions still require staging validation. Rebuild downstream extensions against the updated headers when possible.

The security backport terminates a worker immediately on PHP memory exhaustion rather than attempting unsafe cleanup; shutdown callbacks may not run after OOM. The added multipart limit defaults to `max_input_vars + max_file_uploads` and can reject unusually large multipart requests. Review applications that depend on either behavior.

## Build and validate

```sh
docker build --platform linux/amd64 -t devitools/php-apache:7.3 php-apache/7.3
python3 php-apache/7.3/.scripts/smoke.py devitools/php-apache:7.3
```

Build-time checks verify the PHP version, complete extension-name parity, multipart prerequisite, upload progress setting and FPM configuration syntax. Apache configuration syntax is checked by the smoke test after the entrypoint renders its templates. The smoke test starts the normal entrypoint, exercises HTTP through Apache/FPM with a custom document root and PHP environment values, verifies representative extensions, checks configuration mounts and module disabling, and verifies that OOM skips unsafe shutdown callbacks and FPM can serve a subsequent request.

For deployment, change only the image reference in your Compose service and retain its environment, mounts, ports and other settings. Recreate the containers; pulling a new image does not update running workers.

PHP 7.3 and Debian Buster remain end of life. This is a narrowly scoped compatibility bridge for the two named CVEs, not a fully maintained operating system or PHP distribution. Plan migration to a supported PHP branch.
3 changes: 3 additions & 0 deletions php-apache/7.3/variants.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- target: php-apache
suffix: ""
platforms: linux/amd64
Loading