A critical remote code execution (RCE) vulnerability, known as PHP2Shell (CVE-2026-109618 and CVE-2026-109619, CVSS 9.2), affects the PHP bundled with the CloudBees CD/RO web server.
CloudBees CD/RO 2026.10.13 and later delivers PHP 8.5.12, resolves the vulnerability. If you cannot upgrade your server, refer to this workaround to replace the bundled PHP with PHP 8.5.12 for your current version.
| Apply this workaround in a lower environment (development, test, or staging) first. Verify the web UI and your key workflows before you roll it out to production. |
Before you begin
The workaround changes only the CloudBees CD/RO web server. The CloudBees CD/RO server, repository, agents, and database are not modified and keep running.
| CloudBees CD/RO version | Changes made |
|---|---|
2026.03 or later |
Replaces |
Earlier than 2026.03 (Linux) |
|
Earlier than 2026.03 (Windows) |
|
The previous PHP folder is kept as php-old-<timestamp> (Linux) or php-old-<N> (Windows). Modified configuration files are saved as *.backup.
Workaround procedures
| Installation | Procedure | Applies to |
|---|---|---|
Linux VM |
Run the workaround script ( |
All CloudBees CD/RO releases that include a web server on the host. |
Windows VM |
Run the workaround script ( |
All CloudBees CD/RO releases that include a web server on the host. |
Kubernetes (Helm chart |
Chart 2.22.0 and later (CloudBees CD/RO 2023.02 and later). |
Prerequisites
| Requirement | Linux | Windows |
|---|---|---|
Privileges |
|
Elevated Command Prompt |
Platform |
x86_64 with glibc 2.28 or later |
64-bit x86; Windows Server 2019 or Windows 10 version 1803 or later (requires |
Tools |
|
None |
The PHP 8.5.12 libxml2.so.2 library requires glibc 2.28 or later. This includes:
-
RHEL, CentOS Stream, Rocky Linux, and AlmaLinux 8 and 9.
-
Ubuntu 20.04 and later.
-
Debian 10 and later.
Applicable hosts
-
Every host that runs the CloudBees CD/RO web server (Apache).
-
A host that runs only the CloudBees CD/RO server, without a web server, does not need the workaround.
-
In clustered environments, apply it on every web server node, one node at a time.
| The web server is stopped during the PHP swap, which takes about one minute. The web UI is unavailable during that time. |
Server installation and data directories
The server installation and data directories can be the same directory or different ones, depending on your installation.
- Linux
-
Run:
ps -eo args | grep '[h]ttpd' | head -1-
<INSTALL>is the part of the path before/apache/bin/httpd. -
<DATA>is the part of the path before/apache/conf/httpd.conf(the value after-f).
-
- Windows
-
Run:
sc qc CommanderApacheIn
BINARY_PATH_NAME:-
<INST>is the part before\apache\bin\httpd.exe. -
<DATA>is the part before/apache/conf/httpd.conf(the value after-f).
-
Workaround for Linux or Windows
The script detects your CloudBees CD/RO version from <INSTALL>/bin/ectool --version. If detection fails, set CD_VERSION (for example, CD_VERSION=2026.09.14.187032) and run the script again.
-
Download the script to the web server host.
- Linux
-
curl -fLO https://storage.googleapis.com/cloudbees-cd-artifacts/php-8.5.12-patch-source/apply-php-workaround.sh chmod +x apply-php-workaround.sh - Windows
-
curl.exe -fLO https://storage.googleapis.com/cloudbees-cd-artifacts/php-8.5.12-patch-source/apply-php-workaround.bat
If the host has no internet access, download the script on another machine and copy it to the host.
-
Run the script with the directories from Server installation and data directories.
- Linux
-
# CloudBees CD/RO 2026.03 or later sudo ./apply-php-workaround.sh --install-dir <INSTALL> # CloudBees CD/RO earlier than 2026.03 sudo ./apply-php-workaround.sh --install-dir <INSTALL> --data-dir <DATA>If the script cannot find the web server init script, it lists the contents of
/etc/init.dand asks which one starts the web server. Leave it blank to stop and start the web server yourself. - Windows
-
:: {PRODUCT} 2026.03 or later apply-php-workaround.bat --install-dir "<INST>" :: {PRODUCT} earlier than 2026.03 apply-php-workaround.bat --install-dir "<INST>" --data-dir "<DATA>"If the
CommanderApacheservice is not found, the script lists candidate services and asks for the service name. Leave it blank to stop and start the web server yourself.
Table 1. Script options Script option Description --install-dir DIRRequired. The CloudBees CD/RO install directory.
--data-dir DIRRequired for CloudBees CD/RO earlier than 2026.03. The directory that contains
apache/conf.--patch FILEOptional. Path to a local PHP 8.5.12 zip file. Use this on hosts without internet access.
If the host already runs PHP 8.5.12, the script prints
already running PHP 8.5.12, nothing to doand exits. -
Confirm that the output ends with:
==> web server is running ==> PHP before: PHP <old version> ... ==> PHP after: PHP 8.5.12 ... ==> backup of the previous PHP kept at <install-dir>/php-old-<timestamp>
Hosts without internet access
If the download fails with no data received - this server appears to have no internet access:
-
On a machine with internet access, download the PHP 8.5.12 package:
-
Copy the file to the web server host.
-
Run the script again with
--patch:# Linux sudo ./apply-php-workaround.sh --install-dir <INSTALL> [--data-dir <DATA>] --patch /tmp/php.8.5.12-x86_64_Linux.zip:: Windows apply-php-workaround.bat --install-dir "<INST>" [--data-dir "<DATA>"] --patch "C:\Temp\php.8.5.12-i686_win64.zip"
Validate the workaround
-
Check the PHP version:
- Linux
-
<INSTALL>/php/bin/php --version | grep '^PHP 'The output must start with
PHP 8.5.12. Alibxml2.so.2: no version information availablemessage is harmless. - Windows
-
"<INST>\php\php.exe" -v
-
Sign in to the CloudBees CD/RO web UI and open several pages, including your key workflows.
-
Check the Apache error log (
<DATA>/apache/logs/error.log) for errors.PHP Deprecatednotices from CloudBees CD/RO code are expected and harmless.
Rollback on the web server
If any step fails after the web server stops, the script restores the original PHP folder (and, for releases earlier than 2026.03, php.ini, php.conf, or vcruntime140.dll) and restarts the web server. A confirmation message Original PHP restored and the web server is running. displays.
On Windows, if you interrupt the script after confirmation stopping web server, use the manual undo steps.
|
Manual undo
Use these steps to revert the workaround after it completes, or if the script fails and cannot roll back.
- Linux
-
<service> stop rm -rf <INSTALL>/php mv <INSTALL>/php-old-<timestamp> <INSTALL>/php # CloudBees CD/RO earlier than 2026.03 only: cp -p <DATA>/apache/conf/php.ini.backup <DATA>/apache/conf/php.ini cp -p <DATA>/apache/conf/conf.d/php.conf.backup <DATA>/apache/conf/conf.d/php.conf <service> start<service>is the init script or systemd unit that starts and stops the web server. - Windows
-
net stop CommanderApache rmdir /S /Q "<INST>\php" ren "<INST>\php-old-1" php :: CloudBees CD/RO earlier than 2026.03 only: copy /Y "<INST>\apache\bin\vcruntime140.dll.backup" "<INST>\apache\bin\vcruntime140.dll" copy /Y "<DATA>\apache\conf\php.ini.backup" "<DATA>\apache\conf\php.ini" net start CommanderApacheUse the actual
php-old-<N>folder name in<INST>.
Apply the workaround manually
Use these steps only if you cannot run the script, or if the script failed and could not roll back. Download the PHP 8.5.12 zip first (for more information, refer to Hosts without internet access).
Linux manual steps
-
Stop the web server and confirm it is stopped:
<service> stop pgrep -x httpd # must print nothing -
Unpack PHP 8.5.12 next to the existing folder and swap them:
mkdir <INSTALL>/php-new unzip -q <zip> -d <INSTALL>/php-new # bin/php must be directly in php-new chmod 755 <INSTALL>/php-new/bin/* <INSTALL>/php-new/ext/* chown -R "$(stat -c '%u:%g' <INSTALL>)" <INSTALL>/php-new mv <INSTALL>/php <INSTALL>/php-old-manual mv <INSTALL>/php-new <INSTALL>/php <INSTALL>/php/bin/php --version | grep '^PHP ' # expect PHP 8.5.12 -
CloudBees CD/RO earlier than 2026.03 only. With the web server still stopped, update the Apache and PHP configuration:
CONFDIR=<DATA>/apache/conf # Back up (skip if a .backup file already exists) [ -e $CONFDIR/php.ini.backup ] || cp -p $CONFDIR/php.ini $CONFDIR/php.ini.backup [ -e $CONFDIR/conf.d/php.conf.backup ] || cp -p $CONFDIR/conf.d/php.conf $CONFDIR/conf.d/php.conf.backup # Remove session.sid_length and disable_classes from php.ini grep -vE '^(session\.sid_length|disable_classes)[[:space:]]*=' $CONFDIR/php.ini > /tmp/php.ini.new cat /tmp/php.ini.new > $CONFDIR/php.iniThen add the following block before the
LoadModule php_moduleline in$CONFDIR/conf.d/php.conf, replacing<INSTALL>with your install path:<IfFile "<INSTALL>/php/lib/libxml2.so.2"> LoadFile "<INSTALL>/php/lib/libxml2.so.2" </IfFile>The
<IfFile>guard makes Apache skip the directive if the file is removed, for example after a rollback or upgrade.Use the libxml2.so.2shipped inphp/lib. Do not replace the shared<INSTALL>/lib/libxml2.so.2or use the systemlibxml2. The system library is too old for PHP 8.5.12, and the CloudBees CD/RO agent on the same host also loads the shared library. Ifphp/lib/libxml2.so.2is missing, your zip is outdated; download it again. -
Check the configuration and start the web server:
<INSTALL>/apache/bin/httpd -t -f <DATA>/apache/conf/httpd.conf # must print Syntax OK <service> start pgrep -x httpd # must print PIDsIf you have no separate data directory, use
<INSTALL>in place of<DATA>.
Windows manual steps
Run these commands in an elevated Command Prompt.
-
Set variables, using the values from Server installation and data directories:
set "INST=<INST>" set "DATA=<DATA>" set "ZIP=C:\Temp\php.8.5.12-i686_win64.zip" set "NEWDIR=%TEMP%\phpnew" -
Unpack PHP 8.5.12, stop the web server, and swap the folders:
mkdir "%NEWDIR%" %SystemRoot%\System32\tar.exe -xf "%ZIP%" -C "%NEWDIR%" dir "%NEWDIR%\php.exe" net stop CommanderApache ren "%INST%\php" php-old-1 move "%NEWDIR%" "%INST%\php" icacls "%INST%\php" /reset /T /C /Q "%INST%\php\php.exe" -vphp.exemust be directly inside%INST%\php. Ifphp-old-1already exists, use the next free number. Ifmovefails across drives, usexcopy /E /I /Q "%NEWDIR%" "%INST%\php". -
CloudBees CD/RO earlier than 2026.03 only. With the web server still stopped, update the VC runtime and `php.ini`. Apache in these releases ships VC runtime 14.16, which PHP 8.5.12 cannot load (Apache fails with
AH00020 Configuration Failed).copy /Y "%INST%\apache\bin\vcruntime140.dll" "%INST%\apache\bin\vcruntime140.dll.backup" copy /Y "%INST%\php\vcruntime140.dll" "%INST%\apache\bin\vcruntime140.dll" copy /Y "%DATA%\apache\conf\php.ini" "%DATA%\apache\conf\php.ini.backup" findstr /V /R /C:"^session\.sid_length" /C:"^disable_classes" "%DATA%\apache\conf\php.ini.backup" > "%DATA%\apache\conf\php.ini"If a
.backupfile already exists from an earlier run, do not overwrite it. Skip thatcopycommand and read from the existing backup. -
Start the web server and verify:
net start CommanderApache "%INST%\php\php.exe" -v echo ^<?php echo PHP_SAPI, ' ', PHP_VERSION; > "%INST%\apache\htdocs\commander\zz-check.php" curl -k https://localhost/commander/zz-check.php del "%INST%\apache\htdocs\commander\zz-check.php" powershell -NoProfile -Command "Get-Content -Tail 30 '%DATA%\apache\logs\error.log'"The
curloutput must beapache2handler 8.5.12, anderror.logmust not containConfiguration Failed.
Troubleshooting
Details
| Message | Cause and resolution |
|---|---|
|
Add |
|
The |
|
Set |
|
Use |
|
The script rolls back and restarts the web server. Find out why |
|
The host runs a Windows version earlier than Server 2019 or Windows 10 version 1803. Extract the zip on another machine, or use the manual steps with another unzip tool. |
|
The script has already rolled back. Check |
Apache fails with |
Apache is loading an older |
Apache fails with |
Apache is using the system |
Apply the workaround on Kubernetes
This procedure applies to Helm chart cloudbees/cloudbees-flow 2.22.0 and later (CloudBees CD/RO 2023.02 and later).
CloudBees does not publish patched images for this workaround. You build a patched web image in your environment, push it to a separate repository with your release’s own tag (cbflow-web-php8.5.12:<release-tag>), and point the flow-web pods to it with web.imageRepository.
Only the flow-web pods change. The server, repository, agents, and database keep their images and are not restarted. The web UI is unavailable only while the web pod is replaced.
Do not use web.imageTag for this workaround. Charts earlier than 2.33.0 (CloudBees CD/RO 2025.03) ignore that setting, so the pods would keep the original image without warning.
|
Requirements
-
An x86_64 machine with Docker or Podman
-
kubectlandhelmconfigured for your cluster -
Internet access to download the Kubernetes package
-
Push access to the registry your cluster pulls CloudBees CD/RO images from
The examples use CloudBees CD/RO 2025.12.2. Replace these values with your own:
| Value | Example | Used in |
|---|---|---|
Web image |
|
|
Release tag |
|
|
New repository |
|
Step 3 and |
Chart version |
|
|
Chart repository |
|
Step 4, if you install the chart from your own mirror |
Also replace <namespace>, <release>, and <your-registry> with your CloudBees CD/RO namespace, Helm release name, and registry.
-
Identify your chart version and web image:
helm list -A | grep cloudbees-flow kubectl -n <namespace> get deploy flow-web -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}' -
Download and unpack the Kubernetes package, which contains
kubernetes/Dockerfileand the Linux PHP 8.5.12 bundle:curl -fLO https://storage.googleapis.com/cloudbees-cd-artifacts/php-8.5.12-patch-source/php-8.5.12-k8s.zip unzip php-8.5.12-k8s.zip -d php-8.5.12-k8s cd php-8.5.12-k8s -
Build and push the patched image:
docker build -f kubernetes/Dockerfile \ --build-arg BASE=docker.io/cloudbees/cbflow-web:2025.12.2.184668_3.2.201_20260222 \ -t <your-registry>/cbflow-web-php8.5.12:2025.12.2.184668_3.2.201_20260222 . docker push <your-registry>/cbflow-web-php8.5.12:2025.12.2.184668_3.2.201_20260222If the build machine cannot reach Docker Hub, use the same base image from your own registry. Some registries, such as Amazon ECR, require you to create the
cbflow-web-php8.5.12repository before the first push. -
Point the web pods to the patched image.
--versionkeeps your current chart version and--reuse-valueskeeps your existing settings:helm repo update cloudbees helm upgrade <release> cloudbees/cloudbees-flow -n <namespace> --version 2.36.2 --reuse-values \ --set web.imageRepository=cbflow-web-php8.5.12If the
cloudbeesHelm repository is not configured, add it first:helm repo add cloudbees https://public-charts.artifacts.cloudbees.com/repository/public/Also set
imageRepository: cbflow-web-php8.5.12underweb:in your values file, so later upgrades that use that file keep the fix. -
Verify the rollout and PHP version:
kubectl -n <namespace> rollout status deploy/flow-web kubectl -n <namespace> exec deploy/flow-web -- sh -c 'LD_PRELOAD=/opt/cbflow/php/lib/libxml2.so.2 /opt/cbflow/php/bin/php -v | head -1'The second command must print
PHP 8.5.12 (cli). Then sign in to the web UI and open several pages.
Pull the patched image from a different registry
The chart builds every image reference from one registry setting (images.registry, or global.cloudbees.imageRegistry when it is set). Using only --set web.imageRepository=cbflow-web-php8.5.12 therefore makes the web pod look for the patched image in the same registry as the other CloudBees CD/RO images.
If your cluster pulls CloudBees CD/RO images straight from docker.io/cloudbees, you cannot push the patched image there. Instead of mirroring every image, you can keep the other components on Docker Hub and pull only the web image from a registry you control. Push the patched image to that registry (step 3), then clear the shared registry setting and give each component its full repository path:
helm upgrade <release> cloudbees/cloudbees-flow -n <namespace> --version <chart-version> --reuse-values \ --set images.registry= \ --set server.imageRepository=docker.io/cloudbees/cbflow-server \ --set web.imageRepository=<your-registry>/cbflow-web-php8.5.12
If your installation sets global.cloudbees.imageRegistry, that value takes precedence over images.registry. The command above then gives the web pod an invalid image name and the pod stays in ImagePullBackOff. In that case, also clear the global registry and set images.tag to your release tag (the tag from step 3), because the chart uses global.cloudbees.imageTag only while global.cloudbees.imageRegistry is set:
helm upgrade <release> cloudbees/cloudbees-flow -n <namespace> --version <chart-version> --reuse-values \ --set images.registry= \ --set global.cloudbees.imageRegistry= \ --set images.tag=<release-tag> \ --set server.imageRepository=docker.io/cloudbees/cbflow-server \ --set web.imageRepository=<your-registry>/cbflow-web-php8.5.12
Set <component>.imageRepository=docker.io/cloudbees/cbflow-<component> for every other component that you have enabled, such as the repository server, analytics, and DOIS. If you skip one, its image reference loses the registry prefix and Kubernetes restarts it with a different image. Before you upgrade, run the same command with --dry-run and compare its image: lines with the output of helm get manifest <release> -n <namespace>. Only the flow-web image must differ. Ignore hook images such as cbflowtest/wget.
|
The cluster nodes must be able to pull from <your-registry>. If the registry is private and the nodes have no access, the web pod stays in ImagePullBackOff with an authorization error. Configure an image pull secret for that registry.
To undo this change, run helm rollback <release> -n <namespace>.
Roll back on Kubernetes
helm rollback <release> -n <namespace> kubectl -n <namespace> rollout status deploy/flow-web
helm rollback returns to the previous revision, so the web pods run the original image. If imageRepository was added to the values file, remove it.
Kubernetes considerations
Details
-
The fix is part of the Deployment, so it persists across pod restarts and applies to pods added by the Horizontal Pod Autoscaler (HPA). Do not patch a running pod with
kubectl execorkubectl cp; the change is lost on the next restart. -
The chart takes the registry for every CloudBees CD/RO image from one setting (
images.registry, orglobal.cloudbees.imageRegistrywhen set). Push the patched image to the registry your cluster already uses. If your cluster pulls directly fromdocker.io/cloudbees, you cannot push the patched image there. Either mirror the CloudBees CD/RO images you run to a registry you control, setimages.registryto it, and push the patched image there, or pull only the patched web image from another registry. -
The Dockerfile ends with
USER 1000, the usercbflow-webimages run as. If your image runs as a different user, update that line.
Limitations and risks
-
The workaround changes only PHP on the web server. It does not upgrade Apache, OpenSSL, the shared
libxml2, or CloudBees CD/RO agents. -
Linux hosts with glibc earlier than 2.28 (for example, RHEL or CentOS 7, Ubuntu 18.04) cannot use this workaround.
-
For releases earlier than 2026.03, the workaround removes the
session.sid_lengthanddisable_classessettings fromphp.ini. PHP 8.5 ignoresdisable_classesand deprecatessession.sid_length, which makes PHP log a deprecation notice at every start. The defaultphp.inileavesdisable_classesempty, so removing it does not change behavior. If you setdisable_classesto a non-empty value, PHP 8.5.12 does not enforce it with or without this workaround. Use another control to restrict those classes. -
The web server is unavailable for about one minute on each host or web pod while the workaround is applied.
-
This is a temporary mitigation. Upgrade to CloudBees CD/RO 2026.10.13 or later as soon as possible.
After upgrading to CloudBees CD/RO 2026.10.13 or later
CloudBees CD/RO 2026.10.13 and later delivers PHP 8.5.12, so the workaround is no longer needed.
- Kubernetes
-
Remove the image override before or during the upgrade. Otherwise, the web pod looks for
cbflow-web-php8.5.12with the new release tag, which does not exist. Run the upgrade with--reuse-values --set web.imageRepository=cbflow-web(or your original value), and removeimageRepositoryfrom your values file. If you have used a different registry for the patched image, also restoreimages.registry(defaultdocker.io/cloudbees) and each<component>.imageRepository(defaultcbflow-<component>) that you have set. - Linux and Windows
-
The upgrade installs its own PHP. After you verify the new release, delete the
php-old-folders and.backupfiles created by the workaround.