Mitigate the PHP2Shell vulnerability without an upgrade

12 minute readSecurity

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 <INSTALL_DIRECTORY>/php with PHP 8.5.12. No other files are changed.

Earlier than 2026.03 (Linux)

  • Replaces <INSTALL_DIRECTORY>/php with PHP 8.5.12.

  • Removes the session.sid_length and disable_classes settings from php.ini.

  • Adds a LoadFile directive to conf.d/php.conf so Apache loads the libxml2 library bundled with PHP 8.5.12.

Earlier than 2026.03 (Windows)

  • Replaces <INSTALL_DIRECTORY>\php with PHP 8.5.12.

  • Copies the PHP vcruntime140.dll over the one in apache\bin.

  • Removes the session.sid_length and disable_classes settings from php.ini.

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 (apply-php-workaround.sh).

All CloudBees CD/RO releases that include a web server on the host.

Windows VM

Run the workaround script (apply-php-workaround.bat).

All CloudBees CD/RO releases that include a web server on the host.

Kubernetes (Helm chart cloudbees/cloudbees-flow)

Chart 2.22.0 and later (CloudBees CD/RO 2023.02 and later).

Prerequisites

Requirement Linux Windows

Privileges

root

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 tar.exe, and curl.exe unless you use --patch)

Tools

unzip, and curl or wget (not required with --patch)

None

Supported Linux distributions

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 CommanderApache
▼

In 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.

  1. 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.

  2. 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.d and 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 CommanderApache service 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 DIR

    Required. The CloudBees CD/RO install directory.

    --data-dir DIR

    Required for CloudBees CD/RO earlier than 2026.03. The directory that contains apache/conf.

    --patch FILE

    Optional. 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 do and exits.

  3. 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>
  4. Validate the workaround.

Hosts without internet access

If the download fails with no data received - this server appears to have no internet access:

  1. On a machine with internet access, download the PHP 8.5.12 package:

  2. Copy the file to the web server host.

  3. 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

  1. Check the PHP version:

    Linux
    <INSTALL>/php/bin/php --version | grep '^PHP '
    ▼

    The output must start with PHP 8.5.12. A libxml2.so.2: no version information available message is harmless.

    Windows
    "<INST>\php\php.exe" -v
    ▼
  2. Sign in to the CloudBees CD/RO web UI and open several pages, including your key workflows.

  3. Check the Apache error log (<DATA>/apache/logs/error.log) for errors. PHP Deprecated notices 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 CommanderApache
▼

Use 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
  1. Stop the web server and confirm it is stopped:

    <service> stop pgrep -x httpd # must print nothing
    ▼
  2. 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
    ▼
  3. 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.ini
    ▼

    Then add the following block before the LoadModule php_module line 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.2 shipped in php/lib. Do not replace the shared <INSTALL>/lib/libxml2.so.2 or use the system libxml2. 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. If php/lib/libxml2.so.2 is missing, your zip is outdated; download it again.
  4. 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 PIDs
    ▼

    If you have no separate data directory, use <INSTALL> in place of <DATA>.

Windows manual steps

Run these commands in an elevated Command Prompt.

  1. 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"
    ▼
  2. 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" -v
    ▼

    php.exe must be directly inside %INST%\php. If php-old-1 already exists, use the next free number. If move fails across drives, use xcopy /E /I /Q "%NEWDIR%" "%INST%\php".

  3. 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 .backup file already exists from an earlier run, do not overwrite it. Skip that copy command and read from the existing backup.

  4. 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 curl output must be apache2handler 8.5.12, and error.log must not contain Configuration Failed.

Troubleshooting

Details
Message Cause and resolution

--data-dir is required for CloudBees CD older than 2026.03

Add --data-dir with the directory that contains apache/conf.

<data>/apache/conf/php.ini not found (or php.conf on Linux)

The --data-dir value is wrong. Use the part before /apache/conf/httpd.conf in the service definition. For more information, refer to Server installation and data directories.

could not detect CloudBees CD version

Set CD_VERSION and run the script again. Linux: sudo CD_VERSION=<version> ./apply-php-workaround.sh …​. Windows: set CD_VERSION=<version> first.

no data received - this server appears to have no internet access

Use --patch. For more information, refer to Hosts without internet access.

httpd is still running after 60s; stop it manually and re-run (Linux)

The script rolls back and restarts the web server. Find out why httpd does not stop (for example, hung workers), stop it manually, and run the script again.

tar.exe not found or curl.exe not found (Windows)

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 web server did not start with the new PHP

The script has already rolled back. Check <DATA>/apache/logs/error.log and send it to CloudBees Support.

Apache fails with undefined symbol: xmlTextReaderAttributeCount, version LIBXML2_2.4.30 (manual steps)

Apache is loading an older libxml2. Make sure the LoadFile block for <INSTALL>/php/lib/libxml2.so.2 is in php.conf before LoadModule php_module.

Apache fails with undefined symbol: xmlFreeEntity

Apache is using the system libxml2, which is too old for PHP 8.5.12. Use the libxml2.so.2 from <INSTALL>/php/lib instead of the system library when configuring LoadFile.

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

  • kubectl and helm configured 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

docker.io/cloudbees/cbflow-web:2025.12.2.184668_3.2.201_20260222

--build-arg BASE= (step 3)

Release tag

2025.12.2.184668_3.2.201_20260222

-t and docker push (step 3)

New repository

cbflow-web-php8.5.12

Step 3 and web.imageRepository (step 4). If your web.imageRepository is not the default cbflow-web, use <your-value>-php8.5.12.

Chart version

2.36.2

--version (step 4)

Chart repository

cloudbees/cloudbees-flow

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.

  1. 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"}'
    ▼
  2. Download and unpack the Kubernetes package, which contains kubernetes/Dockerfile and 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
    ▼
  3. 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_20260222
    ▼

    If 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.12 repository before the first push.

  4. Point the web pods to the patched image. --version keeps your current chart version and --reuse-values keeps 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.12
    ▼

    If the cloudbees Helm 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.12 under web: in your values file, so later upgrades that use that file keep the fix.

  5. 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 exec or kubectl 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, or global.cloudbees.imageRegistry when set). Push the patched image to the registry your cluster already uses. If your cluster pulls directly from docker.io/cloudbees, you cannot push the patched image there. Either mirror the CloudBees CD/RO images you run to a registry you control, set images.registry to it, and push the patched image there, or pull only the patched web image from another registry.

  • The Dockerfile ends with USER 1000, the user cbflow-web images 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_length and disable_classes settings from php.ini. PHP 8.5 ignores disable_classes and deprecates session.sid_length, which makes PHP log a deprecation notice at every start. The default php.ini leaves disable_classes empty, so removing it does not change behavior. If you set disable_classes to 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.12 with the new release tag, which does not exist. Run the upgrade with --reuse-values --set web.imageRepository=cbflow-web (or your original value), and remove imageRepository from your values file. If you have used a different registry for the patched image, also restore images.registry (default docker.io/cloudbees) and each <component>.imageRepository (default cbflow-<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 .backup files created by the workaround.