Web Stack Patch Checker (PHP2Shell edition)
Pricing
from $2.00 / 1,000 url checkeds
Web Stack Patch Checker (PHP2Shell edition)
Check your client sites for the PHP2Shell advisory: PHP version from public headers, patched yes/no/unknown, risk tier, fix line. One GET per URL.
Pricing
from $2.00 / 1,000 url checkeds
Rating
0.0
(0)
Developer
Kaizoku Data
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Paste a list of your sites and get one row per URL: the PHP version its public headers show, whether that version indicates the PHP2Shell fix (CVE-2026-109618, CVE-2026-109619), a risk tier and the exact fix line. $2.00 per 1,000 URLs checked (pay per event; blocks, robots.txt refusals and errors are free).
Only check sites you own or manage, or have written permission to check.
- Fleet triage in minutes. Built for agencies, MSPs and hosting resellers with dozens or thousands of client sites: sort by
riskTierand start withhigh. - One normal page request per URL. One GET (redirects followed, at most 5) plus the host's robots.txt. No exploit traffic, no other paths, no probing.
- Exact fix lines.
upgrade to 8.3.36+, or for end-of-life branchesupgrade to a supported branch: 8.3.36+ .... - Honest about what headers can't tell. Distribution builds (
-1ubuntu2.14,+deb12u1), hidden versions and proxies are markedunknownorreview, with aphp -vverify note. - Ready for the next incident. All advisory knowledge sits in one data file; future editions are a data update.
This is a triage of public signals, not a vulnerability scan. A hidden version is not proof of a fix: run php -v on the server. We never say vulnerable or safe; rows say what a version string indicates.
Quick start
- Open the Actor, keep the sample input (a header echo test URL that reports PHP/8.3.30, and a scraping sandbox site) and press Start. It takes about 10 seconds.
- Replace the URLs with the home pages of the sites you manage, one per line.
- Open the Overview table, sort by risk tier, and download it as CSV or Excel, or call the Actor from code (see below).
What is sent, and what is not
| Sent | Never sent |
|---|---|
One plain GET of each URL you list, exactly as given (the fragment after # is dropped, as browsers do) | Exploit payloads, multipart bodies, POST requests |
| The same GET for each redirect hop (at most 5; every hop re-checked: public addresses only, no platform hosts, robots.txt) | Any other path: no /phpinfo.php, /info.php, admin pages or guesses |
/robots.txt once per host (read before anything else, honoured with our exact token) | HEAD requests, retries after a refusal, query changes, fuzzing |
User-Agent web-stack-patch-checker-actor/0.1 (https://apify.com/ataism/web-stack-patch-checker) | Cookies, credentials, proxies, browser disguises |
At most one request per second per host (robots.txt included); a robots.txt Crawl-delay is honoured in full. A 401, 403, 406, 429, 451, 999 or challenge page is recorded and not charged, and after 3 of them the host is not contacted again in that run.
What is read from the response: the X-Powered-By and Server headers, the names of cookies the site sets (never their values) and the <meta name="generator"> tag in the first 256 KB of HTML. No page text is saved.
Risk tiers
| Tier | Meaning | What to do |
|---|---|---|
high | A plain version (no suffix) that indicates an unpatched release (for example 8.3.30 < 8.3.36) or an end-of-life branch (for example 7.4.33 or 8.1.34: no upstream fix) | Update now; see fixLine |
review | PHP is likely (session cookie, WordPress or Drupal generator tag, X-Powered-By: PHP) but the version is hidden; or the version is a distribution build whose fix status the version string can't show, including end-of-life distribution builds such as 8.1.2-1ubuntu2.14 (distribution LTS releases can backport fixes; these rows keep eol: true and the end-of-life fix line); or a pre-release build (8.4.27RC1, 8.3.36-dev) | Run php -v on the server, or check your distribution's advisory |
low | The version indicates the fixed release or later | Confirm on the server if the header may be stale |
none | No PHP signal in headers, cookie names or the generator tag | Not proof that PHP is absent; many sites hide it |
When the Server header shows Apache, the row notes that the advisory rates Apache mod_php as the confirmed attack path (over HTTP we cannot tell mod_php from PHP-FPM). Rows on the 8.2 branch note that its security support ends on 2026-12-31. Every row notes that headers can be stale, set by a proxy or CDN, or reflect only the front server.
The advisory (PHP2Shell)
Checked on 2026-10-22 against the PHP advisories GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp, the php.net releases list and supported versions:
- CVE-2026-109618 "RCE via OOM corruption" and CVE-2026-109619 "RCE via stale global state", both CVSS 4.0 9.2 (
AV:N/AC:H/AT:P/PR:N/UI:N/...), published 2026-10-21. - Affected: PHP
< 8.2.35,< 8.3.36,< 8.4.27,< 8.5.12. Fixed: 8.2.35, 8.3.36, 8.4.27 and 8.5.12, all released on 2026-10-21. - End-of-life branches (8.1 and older) get no upstream fix. We write "end-of-life branch: no upstream fix".
- Server API: the advisory names Apache mod_php as the confirmed application-independent path; FrankenPHP likely; PHP-FPM susceptible by source analysis, with no complete FPM exploit shown; LSAPI likely but unvalidated.
- Mitigation: the only complete one is to update. Interim and partial:
memory_limit=-1enforced administratively, a conservativepost_max_size, and blocking multipart requests at the frontend if your site doesn't use them. - Security support ends: 8.2 on 2026-12-31, 8.3 on 2027-12-31, 8.4 on 2028-12-31, 8.5 on 2029-12-31. 8.1 ended on 2025-12-31 (last release 8.1.34).
Future editions: all incident knowledge (CVE ids, fixed versions per branch, support dates, signal patterns, wording) lives in one data file, src/advisories.json. The next incident (an nginx, WordPress core or OpenSSL-style release) is a data update, and every row names the edition it was checked against (incident).
Input
| Field | What it does |
|---|---|
urls | Pages of sites you own, manage or have written permission to check, one per line. A URL without http:// or https:// is read as https://. At most 5,000 per run; a URL listed twice is checked once (the repeat is a free duplicate row). |
maxConcurrency | URLs checked at the same time across different hosts, 1 to 20, default 10. One host still gets at most 1 request per second. |
Example 1: a client fleet
{"urls": ["https://www.client-one.example/","https://shop.client-two.example/","client-three.example"]}
Example 2: a large list, more hosts in parallel
{"urls": ["https://site-001.example/", "https://site-002.example/", "https://site-003.example/"],"maxConcurrency": 20}
Example 3: the sample input
{"urls": ["https://httpbin.org/response-headers?X-Powered-By=PHP/8.3.30","https://books.toscrape.com/"]}
Output
One row per input URL (the Overview view shows the main columns; Evidence and notes shows the rest):
{"index": 0,"url": "https://httpbin.org/response-headers?X-Powered-By=PHP/8.3.30","finalUrl": null,"status": 200,"detection": "php-version","phpVersion": "8.3.30","versionSuffix": null,"branch": "8.3","patched": "no","eol": false,"securitySupportUntil": "2027-12-31","riskTier": "high","fixLine": "upgrade to 8.3.36+","finding": "version indicates an unpatched release (< 8.3.36)","evidence": [{ "signal": "x-powered-by", "value": "PHP/8.3.30" }],"notes": ["distribution packages (e.g. RHEL) can backport fixes without changing the version string","Headers can be stale, set by a proxy or CDN, or reflect only the front server."],"serverHeader": "gunicorn/19.9.0","incident": "php2shell-2026-10","checkedAt": "2026-10-22T13:04:46.054Z","error": null,"charged": true}
detection:php-version,php-likely, version hiddenorno PHP signal.patched:yes(version indicates the fixed release or later),no(a plain version of an unpatched release or of an end-of-life branch) orunknown(hidden version, distribution build, pre-release build, a branch the data doesn't list, or no signal). An end-of-life distribution build (8.1.2-1ubuntu2.14) isunknownwith tierreview,eol: true, the end-of-lifefixLineand the distribution note.- Pre-release builds (
-dev,RC,alpha,beta) at or above the fixed number areunknownwith tierreview: they may not contain the final release's fix. - When several versions appear (repeated headers, or
X-Powered-ByandServerdisagree), the row uses the lowest one in a header and notes the others. Versions with leading zeros (PHP/08.03.36) or out-of-range parts (PHP/8.3.360) are not read as a patched version. versionSuffix: text after the version, such as-1ubuntu2.14. A distribution build below the fix isunknownwith the note "distribution build: may carry a backported fix; check your distro's advisory". A distribution build of a release at or after the fix isyes.evidence: the signals seen, as{signal, value}:x-powered-by,server,cookie(the name only, for examplePHPSESSID),generator.- A run also writes
RUN_SUMMARYwith counts by detection, risk tier and error code.
Error codes
Rows with an error are not charged.
| Code | Meaning |
|---|---|
blocked-by-robots | robots.txt disallows the URL for this Actor; it was not requested |
robots-unreachable | robots.txt answered 5xx or 429, or could not be reached (after one retry); the host was not requested |
slow-host | robots.txt asks for a Crawl-delay above 10 s, or one that is not a plain number of seconds (1e9, abc, 10s); skipped rather than shortened or guessed |
refused | HTTP 401, 403, 406, 429, 451 or 999; not retried |
challenge | a bot challenge page; not retried |
host-halted | the host refused 3 requests in this run, so it is not contacted again |
server-error | HTTP 5xx; the signals from it are still shown (they may come from a proxy or CDN) |
too-many-redirects | more than 5 redirects |
platform | a social-media, Google or other platform host (refused before any lookup) |
blocked | a private or local address, or a port other than the usual web ports |
invalid-url | not an http(s) URL |
duplicate | the same URL appears earlier in the list |
push-failed | the result could not be saved to the dataset; the run stops with stoppedBy: "error" and a RUN_SUMMARY |
dns, timeout, connection-reset, connection-refused, tls, network | the site could not be reached (no retry: one GET per URL) |
error | anything else |
Pricing
One event, url-checked, at $0.002 per URL whose response was received and analysed, including no PHP signal rows and 404 pages. Free: robots.txt blocks, refusals and challenge pages, 5xx answers, network errors and timeouts, invalid, duplicate, platform and private-address URLs. Apify's standard small per-run start fee applies.
| URLs checked | Price |
|---|---|
| 1 | $0.002 |
| 100 | $0.20 |
| 1,000 | $2.00 |
| 5,000 (one full run) | $10.00 |
Set Max total charge on the run to cap spend; the Actor stops cleanly before a URL the budget cannot pay for.
Limits
- 5,000 URLs per run, at most 20 checked in parallel, 1 request per second per host (a host is its registrable domain and its IP address, so subdomains and names for one server share the rate).
- 30 seconds per URL for all redirect hops, not counting politeness waits. A Crawl-delay above 10 s (at 256 MB) skips the host.
- The run stops (
unpaid-work-cap) once 500 requested URLs gave no charged result and they outnumber the charged ones, or when time spent on uncharged work (robots.txt fetches, refusals, errors) passes 3 s per charged row plus 300 s. The run stops at 240 minutes and keeps its results. - Only what the site shows over HTTP: a CDN or proxy can hide, replace or add headers, and a version string can't show a backported fix.
Responsible use and privacy
- Only check sites you own or manage, or have written permission to check. You are responsible for having the right to check every URL you list.
- The Actor reads public response headers, cookie names and one meta tag, and outputs no page text. Cookie values are never stored. URLs are saved as you gave them, with values of query parameters named like
key,token,secretor session ids (PHPSESSID, names ending insessid,sessor_session) replaced by[redacted]. - It respects robots.txt with its exact product token
web-stack-patch-checker-actor, never works around a block, and uses no proxies.
For AI agents (MCP)
Purpose: one call returns, for each URL, the PHP version its public headers show and whether that indicates the PHP2Shell fix (CVE-2026-109618, CVE-2026-109619), as patched (yes, no, unknown), riskTier (high, review, low, none) and an exact fixLine. One GET per URL plus robots.txt; no exploit traffic. Only for sites the user owns, manages or has written permission to check.
Minimal input that works:
{ "urls": ["https://books.toscrape.com/"] }
Everything else has a default (10 URLs in parallel, 1 request per second per host).
One output row per URL (trimmed): {"url": "https://www.client.example/", "status": 200, "detection": "php-version", "phpVersion": "8.3.30", "patched": "no", "riskTier": "high", "fixLine": "upgrade to 8.3.36+", "finding": "version indicates an unpatched release (< 8.3.36)", "evidence": [{"signal": "x-powered-by", "value": "PHP/8.3.30"}], "error": null}. Rows where error is not null were not checked (robots.txt, refusal, unreachable) and are free. patched: "unknown" means the version is hidden or a distribution build: tell the user to run php -v on the server. Never present a row as proof that a site is or isn't exploitable.
Cost: $0.002 per URL whose response was analysed (1 URL = $0.002, 100 = $0.20, 1,000 = $2.00), plus Apify's standard small per-run start fee. Free: rows with error. Cap spend with Apify's Max total charge.
Limits: at most 5,000 URLs per run; reads only X-Powered-By, Server, cookie names and the generator tag. Headers can be stale or set by a proxy or CDN.
How to call it via the Apify MCP server (https://mcp.apify.com; tool names from the apify-mcp-server README):
search-actorswith a query such asphp version patch checker(free) may listataism/web-stack-patch-checker; search results are ranked, so if it isn't listed, go straight to step 2 with the Actor name.fetch-actor-detailswith actorataism/web-stack-patch-checkerreturns the input schema, pricing and output schema (free).call-actorwithactor=ataism/web-stack-patch-checkerandinput={"urls": ["https://www.client-one.example/", "https://shop.client-two.example/"]}runs it. The response holds run metadata and a dataset ID, not the rows.get-dataset-itemswith that dataset ID returns the rows (follow thenextStepin thecall-actorresponse).
To expose only this Actor as a tool, connect to https://mcp.apify.com?tools=ataism/web-stack-patch-checker.
Use it via API, n8n or Make
Get an API token in Apify Console → Settings → API & Integrations, and keep it in an environment variable (APIFY_TOKEN below). Never paste it into shared code.
cURL (starts a run, waits for it and returns the rows as JSON; this synchronous endpoint waits up to 5 minutes, which fits a few hundred URLs on different hosts; for bigger lists use the clients below):
curl -X POST "https://api.apify.com/v2/acts/ataism~web-stack-patch-checker/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"urls": ["https://www.client-one.example/", "https://shop.client-two.example/"]}'
JavaScript (npm install apify-client):
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('ataism/web-stack-patch-checker').call({urls: ['https://www.client-one.example/', 'https://shop.client-two.example/'],});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const row of items) console.log(row.url, row.riskTier, row.phpVersion, row.fixLine);
Python (pip install "apify-client>=3"):
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("ataism/web-stack-patch-checker").call(run_input={"urls": ["https://www.client-one.example/", "https://shop.client-two.example/"],})for row in client.dataset(run.default_dataset_id).iterate_items():print(row["url"], row["riskTier"], row["phpVersion"], row["fixLine"])
(With apify-client versions before 3.0, call() returns a dict: use run["defaultDatasetId"].)
n8n (check a sheet of client sites):
- Read the URL column with a Google Sheets node and combine it into one list (for example with an Aggregate node).
- Add the Apify node, operation Run an Actor and get dataset, Actor
ataism/web-stack-patch-checker, input{"urls": <your list>}. - Filter on
riskTier=highorreviewand send the rows to your ticketing tool or back to the sheet, matching onurl.
Make:
- Add the Apify module Run an Actor, choose
ataism/web-stack-patch-checker, map your URL list into the input JSON and set Run synchronously to Yes. - Add the Apify module Get Dataset Items and map the run's default dataset ID.
- Add a filter on
riskTierand a Google Sheets, email or ticketing module.
FAQ
Does it prove a site is patched? No. It reads what the site says about itself. A hidden or rewritten header, a CDN in front, or a distribution package with a backported fix can all differ from what PHP really runs. Use it to decide where to look first, then run php -v on the server.
Why does a distribution build show unknown? Debian, Ubuntu and RHEL often backport security fixes without changing the upstream version number, so 8.1.2-1ubuntu2.14 can include a fix that 8.1.2 alone would not. Check your distribution's security advisory for the package.
Why is a site I know runs PHP shown as no PHP signal? Many hosts remove X-Powered-By (expose_php = Off) and rename the session cookie. That is good practice, and it means only a check on the server can tell the version.