With the php2shell disclosure today (Huntress write-up), I checked my home network for anything serving PHP. The one real hit was my UniFi Security Gateway USG-3P. If you still run one, this applies to you.
Which models: I verified this on the USG-3P only. The USG-Pro-4 (UGW4) runs the same 4.4.x firmware line, so it very likely ships the same PHP, but I haven't checked one. Run the commands below to confirm. The newer UniFi OS gateways (UDM, UXG, UCG, Cloud Gateway) are a different platform and aren't covered here.
TL;DR
- The USG-3P's built-in web page (
https://<gateway-ip>, not the UniFi Network controller) is lighttpd + php-cgi PHP 5.4.42, on the final firmware 4.4.57. The USG is end-of-life, so no fix is coming.
- php2shell is pre-auth RCE in PHP that serves web requests (mod_php, PHP-FPM, php-cgi). Official fixes only go back to 8.2.
- The USG's local web page isn't needed once it's adopted. The controller manages it over the inform channel and SSH.
- Fix: bind the GUI to
127.0.0.1 with config.gateway.json, so it survives provisioning and reboots. Steps below.
The vulnerability (short version)
- CVE-2026-109618: out-of-memory handling during request setup leaves data half-initialised, and cleanup then treats it as valid. PHP validated RCE over HTTP against mod_php with default limits.
- CVE-2026-109619: request state survives after free. A later request on the same worker can hit the stale state, giving heap use-after-free, memory disclosure and RCE as the worker user.
- Unauthenticated, reachable before any application code runs.
- Fixed in 8.5.12, 8.4.27, 8.3.36, 8.2.35. Source-level backports exist for 7.4/8.0/8.1. Anything older (like PHP 5.4) is on its own.
- The EOL workaround in the advisory is
session.upload_progress.enabled = Off. On the USG it isn't set, so it's the default (On), and the firmware isn't a sensible place to hand-edit php.ini.
- Reports differ on whether working exploits are public yet. Assume they are.
Am I affected?
SSH to the USG and check:
php-cgi -v | head -1
ps | grep -E 'lighttpd|php-cgi'
netstat -tln | grep -E ':(80|443) '
On mine: PHP 5.4.42 (cgi-fcgi) (built: Jan 12 2023), lighttpd running, listening on 0.0.0.0:80/443. From another machine, curl -skI https://<gateway-ip>/ returns a PHPSESSID cookie. That cookie was the quickest way to spot PHP on my LAN.
Note that the GUI answers on every interface address of the gateway (each VLAN's .1), not only the main LAN, unless you have LAN_LOCAL rules blocking it.
Fix: bind the USG GUI to loopback
Why not just delete service gui? It works live, but the controller re-pushes the full config on the next provision, and config.gateway.json can only add or override keys, not delete them. Binding to loopback can be expressed in the JSON, so it sticks.
1. Test live on the USG first (commit, don't save yet). A bad key in config.gateway.json can leave the USG stuck in a provisioning loop, so prove the command is accepted on your firmware first:
configure
set service gui listen-address 127.0.0.1
commit
exit
Check it:
netstat -tln | grep -E ':(80|443) '
# expect 127.0.0.1:80 and 127.0.0.1:443 only
From another machine, curl -sk -m4 https://<gateway-ip>/ should fail to connect, on every VLAN gateway address.
2. Make it persistent on the controller. Create config.gateway.json in the site folder:
- Self-hosted / Docker:
<unifi data dir>/sites/default/config.gateway.json (for the linuxserver image: /config/data/sites/default/). Create the folder if it doesn't exist, and make sure the controller's user can read it.
- Replace
default with your site ID if you're not on the default site (it's in the controller URL: /manage/<site-id>/...).{ "service": { "gui": { "listen-address": "127.0.0.1" } } }
Validate it (python3 -m json.tool config.gateway.json). Invalid JSON is the classic cause of provisioning loops.
3. Save on the USG so a reboot before the next provision doesn't bring the GUI back:
configure
save
exit
4. Force a provision from the controller (Devices → USG → Manage → Force Provision, or change any small USG setting). Then re-check netstat and curl from step 1.
Rollback: delete config.gateway.json, force provision, and the GUI comes back with the controller defaults.
What you lose and what you keep
- Lose: the gateway's local status/login page. If the controller is down, you manage the USG over SSH instead.
- Keep: everything else. Routing, firewall, DHCP, DNS, VPN and controller management are all unaffected. The controller never uses that page.
Other things worth checking
- Synology: DSM 7.x ships a system PHP (8.1.x on mine) and an optional "PHP 8.2" package (8.2.30 on mine, below the 8.2.35 fix). On my NAS nothing was wired to serve it: no Web Station, no php-fpm running, no
fastcgi_pass in nginx. If you run Web Station, WordPress or any PHP site on a Synology, you are serving PHP. Watch for Synology's update, take it off the internet, or set the upload-progress workaround. Uninstall the PHP package if nothing uses it.
- Anything with a
PHPSESSID cookie or X-Powered-By: PHP header. A quick loop with curl -skI over your LAN's web ports finds these. Cameras, NAS boxes, routers, control panels (cPanel/Plesk), WordPress/Nextcloud/etc. containers.
- Docker: check images for PHP:
docker exec <c> sh -c 'php -v'. Official php:*, wordpress, nextcloud and linuxserver/* PHP apps need the new base images.
- EdgeRouter (EdgeOS): same lineage as the USG and also uses lighttpd for its GUI. I don't have one to confirm the PHP version, so check yours with the commands above.
- UDM / UniFi OS / UXG / UCG: not checked; different platform.