NetScalerSecurity· Koen Warson

php2shell: NetScaler, PHP and what to do today

On 21 October 2026 at 14:00 UTC the PHP security team released PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, fixing two critical vulnerabilities that had been leaking out under the name “php2shell” for two days. Both allow unauthenticated remote code execution and both carry a CVSS v4 score of 9.2. The patch diff is public, proof-of-concept code has been circulating since before the release, and the first exploitation attempts are expected within hours rather than days.

If you run NetScaler, this concerns you twice. Once because the NetScaler management interface runs on PHP and is, by our current understanding, vulnerable until Citrix ships a fixed build. And once because the PHP applications you publish through the NetScaler are vulnerable until their PHP is updated – which is where the NetScaler can help rather than hurt. This post covers both: what the vulnerabilities are, what the exposure on the appliance really is and how to contain it with ACLs today, and how responder policies can shield your PHP backends while the real fix works its way through change windows.

Update – 23 October 2026

Citrix has confirmed the NetScaler management interface is affected. On 21 October Citrix published a TechZone post, “Protecting NetScaler Management Access from PHP CVE-2026-109618 & CVE-2026-109619”. The management GUI and NITRO API on the NSIP, on any SNIP with management access enabled, and on the Cluster IP are affected on all supported NetScaler versions. NetScaler Console (service and on-premises) and the SVM are not affected. A fixed build is not yet available; Citrix will update the bundled PHP package and amend its post when it is.

Citrix’s recommended mitigation is the one described below: restrict GUI and API access on the management addresses to trusted administrative hosts or tightly scoped management networks using ACLs (see also CTX228148, “How to Restrict NetScaler Management Interfaces with ACLs”). Three additions from Citrix’s guidance worth repeating: preserve NetScaler Console / ADM connectivity, keep a recovery path such as serial console access before adding DENY rules, and scope ACLs to the management ports on SNIPs that also carry application traffic. Include the Cluster IP in clustered deployments.

Exploitation status. Security vendors, including watchTowr, report active exploitation attempts against both CVEs through honeypot and customer telemetry; PHP has not confirmed exploitation and, as of this update, neither CVE appears on the CISA Known Exploited Vulnerabilities Catalog. Fixed PHP versions are unchanged: 8.5.12, 8.4.27, 8.3.36 and 8.2.35.

The paragraph “The management interface is affected” below was written before Citrix’s statement and references independent analysis; it stands, with the vendor confirmation above.

What was actually fixed

The two issues are different in mechanism but share one entry point: the way PHP parses multipart/form-data requests.

CVE-2026-109618 – RCE via OOM corruption. PHP uses longjmp() to abort a request when it runs out of memory. If that happens while internal request data is only partially initialised, request shutdown treats the invalid data as valid. An attacker drives PHP towards the memory limit with a multipart request that has no usable Content-Length (chunked transfer) and keeps streaming body data, with parts carrying an unlimited number of headers, or with mass empty file fields that are not counted by max_file_uploads. On Apache with mod_php this has been validated as full unauthenticated RCE with default limits, before a single line of application code runs. PHP-FPM is partially protected because a child that fails startup simply exits.

CVE-2026-109619 – RCE via stale global state. A use-after-free: request-owned state survives after its memory is freed, and a later request handled by the same worker consumes it before normal initialisation resets it. The confirmed path is the session upload-progress feature, which is enabled by default and does not even require session_start(). A first upload triggers an out-of-memory condition during input processing, a later upload-progress operation reads the stale state. This one is validated on both mod_php and FPM.

PHP’s advisory is unusually direct about mitigations: set memory_limit=-1 via php_admin_value, keep post_max_size low, disable session.upload_progress, and – quoting the advisory – “if the application doesn’t use multipart form data, block those requests at the frontend before they reach PHP.” That last sentence is where the NetScaler comes in.

The NetScaler itself: the management interface is exposed

Let us be precise rather than reassuring.

The data plane is not affected. The Gateway, AAA and load-balancing virtual servers are implemented in the packet engine; there is no PHP in the data path, so a crafted multipart request hitting a VIP never reaches a PHP parser on the appliance.

The management interface is affected, and that is not a small thing. The management GUI on the NSIP – and on any SNIP or MIP with management access enabled – runs on Apache with mod_php and an end-of-life PHP 8.1. Remko Weijnen analysed the current firmware and concludes that the vulnerable code is present and reachable before NetScaler authentication; his post, “NetScaler and PHP2Shell”, has the full analysis and is worth reading in full. Citrix / Cloud Software Group has not published a statement at the time of writing, so the finding is vendor-unconfirmed. We are treating it as real, and we recommend you do the same.

There is no “update PHP” option on an appliance: the fix depends on Citrix shipping a new build. Until then, the only lever you have is exposure. A compromised management plane is a compromised NetScaler – every certificate, every backend, every session that passes through it. That is why we are not comfortable with “it’s just the management interface”, and why the rest of this section is about reducing who can reach it.

Isolation is necessary but not sufficient

Your management interfaces should already be on a dedicated, isolated management network, not reachable from the internet and not reachable from the user LAN. If that is not the case, fix it today, before anything else in this post. And check for the unusual: an LB or CS virtual server that forwards to port 80 of the NSIP, usually for a long-forgotten reason, makes the appliance internet-exposed regardless of how well the management VLAN is firewalled.

But even with a properly isolated management network, we recommend additionally protecting the appliances with ACLs on the NetScaler itself. Two reasons. First, “isolated” tends to erode: a jump host gets a second interface, a monitoring server moves, a firewall rule is widened for a migration and never narrowed again. An ACL on the appliance holds regardless of what happened upstream. Second, the threat model for this CVE is not only the internet. An insider, a compromised workstation on a flat network, or lateral movement after an unrelated breach all end up on the management VLAN eventually. Reducing the set of hosts that can talk to the management services from “everything on the VLAN” to “these five addresses” is a real reduction of risk, and it is reversible in one command.

How to set up management ACLs on the NetScaler

NetScaler extended ACLs are evaluated in the packet engine before the request reaches Apache, which is what makes them suitable here. The procedure below takes about an hour per appliance pair if the inventory is done properly.

Step 1 – Inventory the management addresses and services. List every IP on the appliance that has management access, and disable it where it is not needed. These changes take effect immediately, so confirm you are targeting the right SNIP or MIP before running them – never the NSIP, which always needs management access:

show ns ip
# NSIP always has management access.
# For every SNIP/MIP that shows management access enabled and does not need it:
set ns ip <SNIP> -mgmtAccess DISABLED
# For the ones that must keep it, force HTTPS-only on the GUI:
set ns ip <SNIP> -gui SECUREONLY
set ns ip <NSIP> -gui SECUREONLY
save ns config

The management services to cover are SSH (22), the GUI and NITRO API (80 and 443), and the HA / Console communication ports (3008 and 3010). SNMP (161) and syslog are usually outbound or UDP and are handled separately.

Step 2 – Inventory the legitimate sources. Before writing a single DENY, know who must keep talking to the NSIP. Typical list:

  • Admin workstations or, better, the jump hosts your admins use
  • The HA peer’s NSIP (all protocols – HA heartbeat, config sync and file sync must not be filtered)
  • Cluster peers, if applicable
  • NetScaler Console / ADM agent
  • Monitoring (SNMP poller, uptime checks against the GUI)
  • Backup and automation (Ansible, Terraform, scripts using NITRO)
  • SIEM collectors, if they pull rather than receive

If you are not sure, let the ACLs run in a logging-only configuration first: an ALLOW rule with -logstate ENABLED at low priority will show you every source that hits the management ports.

Step 3 – Create the ACLs. Lower priority number wins. ALLOW the inventory, DENY the rest, log the denies. NetScaler ACLs match a single port or a contiguous range per rule, not a comma-separated list, so the rules below reuse a policy dataset for the management ports, with a small second dataset for the two non-adjacent ports the automation hosts need:

add policy dataset DS_mgmt_ports number
bind policy dataset DS_mgmt_ports 22
bind policy dataset DS_mgmt_ports 80
bind policy dataset DS_mgmt_ports 443
bind policy dataset DS_mgmt_ports 3008
bind policy dataset DS_mgmt_ports 3010

add policy dataset DS_autom_ports number
bind policy dataset DS_autom_ports 22
bind policy dataset DS_autom_ports 443

# Admin jump hosts to the management services
add ns acl ACL_MGMT_ALLOW_ADMIN  ALLOW -srcIP = 192.0.2.10-192.0.2.20 -destIP = <NSIP> -protocol TCP -destPortDataset DS_mgmt_ports -priority 10

# HA peer: allow everything between the two NSIPs
add ns acl ACL_MGMT_ALLOW_HAPEER ALLOW -srcIP = <HA_PEER_NSIP> -destIP = <NSIP> -priority 20

# NetScaler Console / ADM agent
add ns acl ACL_MGMT_ALLOW_ADM    ALLOW -srcIP = <ADM_AGENT_IP> -destIP = <NSIP> -protocol TCP -destPortDataset DS_mgmt_ports -priority 30

# Monitoring and automation
add ns acl ACL_MGMT_ALLOW_MON    ALLOW -srcIP = <MONITORING_IP> -destIP = <NSIP> -protocol TCP -destPort = 443 -priority 40
add ns acl ACL_MGMT_ALLOW_AUTOM  ALLOW -srcIP = <AUTOMATION_IP> -destIP = <NSIP> -protocol TCP -destPortDataset DS_autom_ports -priority 41

# Everything else to the management ports: deny and log
add ns acl ACL_MGMT_DENY_REST    DENY  -destIP = <NSIP> -protocol TCP -destPortDataset DS_mgmt_ports -priority 100 -logstate ENABLED

apply ns acls
save ns config

Repeat the set for every SNIP that kept management access, with that SNIP as -destIP. Do it on both nodes of an HA pair; ACLs are part of the synchronised configuration, but verify with show ns acl on the secondary.

Step 4 – Verify before you walk away.

show ns acl
stat ns acl

From an allowed host, SSH and GUI access must keep working. From a disallowed host on the same VLAN, the connection must time out, and the deny must appear in syslog.

Then watch the hit counter on ACL_MGMT_DENY_REST for a day. Every hit is either an attacker, or a legitimate source you forgot in step 2. Both are worth knowing.

What this does and does not do. The ACLs narrow the attack surface from “anyone who can route to the management VLAN” to “these named hosts”. They do not fix the PHP; an allowed admin workstation that gets compromised still has a path to a vulnerable mod_php. They are a temporary measure, to be kept until the Citrix build is installed, and a good practice to keep afterwards.

Remko’s post also discusses configuration stopgaps on the appliance itself. They are unsupported on a production NetScaler and come with side effects, so we prefer the ACL route and a short wait for the vendor build, which we will plan through the usual process for our managed customers as soon as it is released.

Your PHP applications behind the NetScaler: virtual patching

The second half of the story is the one where the NetScaler helps. Every PHP application published through it – CMS platforms, portals, e-commerce sites, intranet tools, APIs, vendor appliances – is exposed until its PHP runtime is updated. Patching takes time, particularly where vendors or change windows are involved. In that window the responder policies below implement the frontend filtering PHP recommends, on the LB or CS virtual servers in front of PHP backends – never on Gateway or AAA vservers, where there is nothing to protect and plenty to break.

The syntax used below (responder actions and policies, policy expressions, pattern sets and audit message actions) is unchanged between NetScaler 13.1 and 14.1, so the configuration applies to both releases as shown.

We split the filtering in three layers so that each customer can choose how far to go:

  • Layer 1 – targeted. Rejects multipart POSTs carrying the PHP_SESSION_UPLOAD_PROGRESS field, the consumer side of the confirmed CVE-2026-109619 path. Legitimate applications almost never use this field, so it is safe to deploy broadly.
  • Layer 2 – strict. Rejects every multipart POST except to an allow-list of known upload endpoints. This is PHP’s own recommendation and covers both CVEs, but it needs an application-specific allow-list and therefore some care.
  • Layer 3 – chunked and oversized multipart. Rejects multipart POSTs without a Content-Length header or above a size limit aligned with the application’s post_max_size. This targets the CVE-2026-109618 OOM path. Some upload SDKs legitimately use chunked encoding, so audit before blocking.

Every policy logs to syslog with a LOGTYPE=php2shell marker, so you also get visibility of who is probing your applications – which turns out to be useful well beyond this week.

These objects and policies take effect as soon as they are bound, so create and test them with the NOOP action first, as described in How we roll it out below.

Common objects

add audit messageaction AUDIT_WAF_php2shell WARNING q{"LOGTYPE=php2shell HOSTNAME=" + HTTP.REQ.HOSTNAME.SERVER + " HTTP_REQ=" + HTTP.REQ.URL + " SRC_IP=" + CLIENT.IP.SRC + " DST_IP=" + CLIENT.IP.DST + " USER-AGENT=" + HTTP.REQ.HEADER("User-Agent")}

add responder action RS_ACT_php2shell_block respondwith q{"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"}

Layer 1 – upload-progress field

add policy expression EXP_WAF_php2shell_uploadprogress q{HTTP.REQ.METHOD.EQ("POST") && HTTP.REQ.HEADER("Content-Type").SET_TEXT_MODE(IGNORECASE).CONTAINS("multipart/form-data") && HTTP.REQ.BODY(65536).SET_TEXT_MODE(IGNORECASE).CONTAINS("PHP_SESSION_UPLOAD_PROGRESS")}

add responder policy RS_PL_WAF_php2shell_uploadprogress EXP_WAF_php2shell_uploadprogress RS_ACT_php2shell_block -logAction AUDIT_WAF_php2shell

Layer 2 – multipart only to known upload endpoints

add policy patset PS_php2shell_upload_allow
bind policy patset PS_php2shell_upload_allow "/upload"
bind policy patset PS_php2shell_upload_allow "/api/files"
bind policy patset PS_php2shell_upload_allow "/wp-admin/async-upload.php"

add policy expression EXP_WAF_php2shell_multipart q{HTTP.REQ.METHOD.EQ("POST") && HTTP.REQ.HEADER("Content-Type").SET_TEXT_MODE(IGNORECASE).CONTAINS("multipart/form-data") && !HTTP.REQ.URL.PATH.SET_TEXT_MODE(IGNORECASE).STARTSWITH_ANY("PS_php2shell_upload_allow")}

add responder policy RS_PL_WAF_php2shell_multipart EXP_WAF_php2shell_multipart RS_ACT_php2shell_block -logAction AUDIT_WAF_php2shell

Layer 3 – chunked / oversized multipart

add policy expression EXP_WAF_php2shell_chunked q{HTTP.REQ.METHOD.EQ("POST") && HTTP.REQ.HEADER("Content-Type").SET_TEXT_MODE(IGNORECASE).CONTAINS("multipart/form-data") && (!HTTP.REQ.HEADER("Content-Length").EXISTS || HTTP.REQ.CONTENT_LENGTH.GT(33554432))}

add responder policy RS_PL_WAF_php2shell_chunked EXP_WAF_php2shell_chunked RS_ACT_php2shell_block -logAction AUDIT_WAF_php2shell

Binding

bind lb vserver <LB_VS_PHP> -policyName RS_PL_WAF_php2shell_uploadprogress -priority 10 -gotoPriorityExpression END -type REQUEST
bind lb vserver <LB_VS_PHP> -policyName RS_PL_WAF_php2shell_chunked        -priority 20 -gotoPriorityExpression END -type REQUEST
bind lb vserver <LB_VS_PHP> -policyName RS_PL_WAF_php2shell_multipart      -priority 30 -gotoPriorityExpression END -type REQUEST
save ns config

Use bind cs vserver for a single binding point on a content-switching vserver.

How we roll it out

The temptation on a day like this is to bind everything in blocking mode and move on. We do not. The sequence that has served us well across React2Shell, the SAML memory-overflow bulletins and now php2shell is simple:

  1. Create the policies with action NOOP and the same -logAction, bind them, and watch stat responder policy and the syslog for an hour or two of normal traffic.
  2. Switch to the blocking action once no legitimate traffic matches (Layers 1 and 3) or the allow-list is complete (Layer 2).
  3. Verify from outside: the requests below should now return 403, with and without chunked transfer encoding.
curl -X POST https://<vip>/index.php -F "PHP_SESSION_UPLOAD_PROGRESS=x" -F "f=@/etc/hostname"
curl -X POST https://<vip>/index.php -H "Transfer-Encoding: chunked" -F "PHP_SESSION_UPLOAD_PROGRESS=x" -F "f=@/etc/hostname"
  1. Leave the policies in place until the backend is confirmed on a fixed PHP release, then remove them in a controlled change.

One tuning note: HTTP.REQ.BODY(65536) makes the ADC buffer up to 64 KB of request body before evaluating Layer 1. On upload-heavy vservers you can lower that to 8192 without losing coverage, since the field appears early in the body.

What this does not do

We are careful with the word “protection” for a reason. The responder policies stop the request patterns described in the advisories. They do not stop application-specific out-of-memory paths that reach CVE-2026-109618 through the application’s own logic, they do not cover the PHP 8.5 fatal_error_backtraces path for CVE-2026-109619, and they cannot anticipate every variation an attacker will come up with once the patch diff has been studied. The ACLs reduce who can reach the management interface; they do not make the PHP behind it any less vulnerable. Both are shields, not fixes.

The fix for your applications is PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35. Linux distributions received the patches in advance; the volunteer Remi repository did not get them on day one. For PHP 7.4, 8.0 and 8.1 the PHP security team published minimal source backports, which is more than end-of-life branches usually get – use them if you build from source, and otherwise plan the migration you have been postponing. The fix for the NetScaler is a Citrix build, and we will tell you the moment there is one.

If you run NetScaler

If Blubyte manages your NetScaler environment, both parts of this post are covered by your service agreement: we review the management-interface exposure and put the ACLs in place, and for every virtual server that publishes a PHP application we deploy the responder policies – tell us which ones to prioritise. If we do not manage your environment, we can do the same as a short fixed-scope engagement, or hand you the configuration above with implementation guidance. Either way, the message is the same one we give every customer: patch what you can patch, reduce exposure on what you cannot, filter in the meantime, and do not mistake any of the last three for the first.

This post reflects the information available from the PHP security team, Citrix, independent researchers and the wider security community on 22 October 2026, updated 23 October 2026. The assessment of the NetScaler management interface is based on independent static analysis; no fixed NetScaler build was available at the time of writing. It may be updated as further details are published. No guarantee of protection is given or implied.

Sources:

Tagged: php2shell, CVE-2026-109618, CVE-2026-109619, PHP security, Responder policy, Virtual patching, NetScaler WAF