php2shell (CVE-2026-109618 / CVE-2026-109619) landed with PHP's October 21 security releases, and GitHub filled up with it within days — by GitHub's own search, at least three exploit-PoC repos created this week are already over 1,300 stars each. OpenAI's contribution to the pile is the opposite of a PoC: openai/php2shell-checker, a defensive tool that tells you whether a site is running a vulnerable PHP — by reading its version banner.
It's 99 lines of Node. It sends no exploit probes. And when I pointed it at 65 real websites, it returned the same answer 65 times: UNKNOWN. The checker is correct about everything it can see. The problem is what websites stopped showing it a decade ago.
What the checker actually does
The real logic lives in skills/phpcheck-url/scripts/phpcheck.js (the top-level phpcheck.js is a 2-line shim). Stripped down:
const FIXED_IN = { '8.2': 35, '8.3': 36, '8.4': 27, '8.5': 12 }
const OLDER_BRANCHES = new Set(['5.4','5.5','5.6','7.0','7.1',
'7.2','7.3','7.4','8.0','8.1'])
It sends a HEAD request (falling back to GET only on 405/501), never follows redirects, never reads the body, and regex-scans the Server and X-Powered-By headers for PHP/x.y.z. If the branch is in FIXED_IN, the patch number decides. If the branch is EOL, the answer is potentially_vulnerable no matter how large the patch number — there is no fixed 8.1, ever, so 8.1.anything is flagged. If both a reassuring and an alarming banner show up, it deliberately reports the alarming one ("Prefer warnings and unrecognized versions over a reassuring banner"). Exit codes: 0 ok, 1 vulnerable, 3 unknown, 2 bad args. Its own test suite: 49/49 passing when I ran it.
As version-string logic goes, this is careful work. So I fed it version strings directly, through a local server emitting crafted X-Powered-By headers:
advertised verdict why
8.2.34 potentially_vulnerable one below the 8.2.35 fix
8.2.35 ok
8.3.36 ok exact boundary
8.4.26 potentially_vulnerable
8.4.27 ok
8.5.11 potentially_vulnerable
8.5.12 ok
8.1.34 potentially_vulnerable EOL branch rule
8.1.99 potentially_vulnerable patch number can't save an EOL branch
7.4.33 potentially_vulnerable
5.6.40 potentially_vulnerable
8.2.34-5ubuntu2 potentially_vulnerable distro suffix parsed, backport invisible
8.2.35RC1 unknown prerelease → refuses to guess
8.6.0 unknown branch not in the table yet
Every boundary is exactly where the PHP advisory says it should be. Note the two honest failures baked into the design, both disclosed in its README: a distro build like Ubuntu's 8.2.34-5ubuntu2 gets flagged even if Ubuntu backported the fix (headers can't tell), and an ok "only describes the advertised version — it doesn't verify the installation."
Then I pointed it at the internet
batch 1: 25 well-known sites (wordpress.com, php.net itself,
Wikipedia, Drupal.org, Joomla.org, Magento, GitHub,
Stack Overflow, BBC, NYT, Reddit, Etsy, Booking, ...)
batch 2: 40 more (phpBB, MediaWiki, Moodle, the GNU/Apache/nginx/
PostgreSQL project sites, language homepages, ...)
result: 65 × UNKNOWN — "PHP version not found"
64 of 65 send no PHP version in any header
1 of 65 (mariadb.org) sends X-Powered-By: Seravo —
a hosting company's name, not a version
Zero vulnerable. Zero ok. Zero information — and that's the finding. expose_php = Off and header-stripping CDNs won so completely that even php.net doesn't advertise PHP. The sites most likely to still run an EOL 8.1 on shared hosting are precisely the ones whose banners, if any, sit behind whatever their host configured — and the checker's target user, per its own skills packaging ("check the PHP version serving my site"), has server access anyway. There's a second skill in the repo, phpcheck-server, that reads the local php -v — that one can't be fooled by a missing banner, because it never looks at one.
What this tool actually is
Read uncharitably, a version-banner checker in 2026 is a metal detector on a beach everyone already swept. Read fairly, it's three useful things:
-
A tripwire for the long tail it can see. The one site in 65 that sent anything sent a hoster name — but small WordPress installs on cheap shared hosting do still leak
X-Powered-By: PHP/8.1.x, and for those, the EOL-branch rule (no patch number absolves 8.1) is exactly the message their owners need, delivered with exit code 1 so it drops into CI. - A model of honest tooling. It refuses to guess on prereleases, refuses to call a backported distro build safe, prints "installed build not verified" on its own all-clear, and never sends a probe. Most security checkers oversell; this one documents its blindness in its own output strings.
- A sign of where checking is moving. OpenAI didn't ship it as a standalone scanner — it's packaged as two agent skills, meant to be installed into Codex and invoked in English ("check https://example.com"). The unit of distribution for a 99-line script is now a skill folder, and the script is almost an implementation detail.
My take: run the server-side check on anything you operate (php -v, compare against 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12, treat anything ≤8.1 as done). Use the URL checker on sites you don't operate with the right expectation: an answer means something, and silence — which is what the modern web returns, 65 times out of 65 — means the banner era is over, not that the PHP underneath is patched.
Sources: github.com/openai/php2shell-checker (checker source, README, test suite), PHP advisory and fixed releases of October 21, 2026 (php.net), GitHub repository search for php2shell PoCs created since 2026-10-17. Header scans and the crafted-version matrix are my own runs.
Top comments (0)