Web Stack Patch Checker (PHP2Shell edition) avatar

Web Stack Patch Checker (PHP2Shell edition)

Pricing

from $2.00 / 1,000 url checkeds

Go to Apify Store
Web Stack Patch Checker (PHP2Shell edition)

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

Kaizoku Data

Maintained by Community

Actor 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 riskTier and start with high.
  • 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 branches upgrade to a supported branch: 8.3.36+ ....
  • Honest about what headers can't tell. Distribution builds (-1ubuntu2.14, +deb12u1), hidden versions and proxies are marked unknown or review, with a php -v verify 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

  1. 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.
  2. Replace the URLs with the home pages of the sites you manage, one per line.
  3. 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

SentNever 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

TierMeaningWhat to do
highA 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
reviewPHP 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
lowThe version indicates the fixed release or laterConfirm on the server if the header may be stale
noneNo PHP signal in headers, cookie names or the generator tagNot 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=-1 enforced administratively, a conservative post_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

FieldWhat it does
urlsPages 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).
maxConcurrencyURLs 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 hidden or no 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) or unknown (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) is unknown with tier review, eol: true, the end-of-life fixLine and the distribution note.
  • Pre-release builds (-dev, RC, alpha, beta) at or above the fixed number are unknown with tier review: they may not contain the final release's fix.
  • When several versions appear (repeated headers, or X-Powered-By and Server disagree), 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 is unknown with 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 is yes.
  • evidence: the signals seen, as {signal, value}: x-powered-by, server, cookie (the name only, for example PHPSESSID), generator.
  • A run also writes RUN_SUMMARY with counts by detection, risk tier and error code.

Error codes

Rows with an error are not charged.

CodeMeaning
blocked-by-robotsrobots.txt disallows the URL for this Actor; it was not requested
robots-unreachablerobots.txt answered 5xx or 429, or could not be reached (after one retry); the host was not requested
slow-hostrobots.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
refusedHTTP 401, 403, 406, 429, 451 or 999; not retried
challengea bot challenge page; not retried
host-haltedthe host refused 3 requests in this run, so it is not contacted again
server-errorHTTP 5xx; the signals from it are still shown (they may come from a proxy or CDN)
too-many-redirectsmore than 5 redirects
platforma social-media, Google or other platform host (refused before any lookup)
blockeda private or local address, or a port other than the usual web ports
invalid-urlnot an http(s) URL
duplicatethe same URL appears earlier in the list
push-failedthe 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, networkthe site could not be reached (no retry: one GET per URL)
erroranything 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 checkedPrice
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, secret or session ids (PHPSESSID, names ending in sessid, sess or _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):

  1. search-actors with a query such as php version patch checker (free) may list ataism/web-stack-patch-checker; search results are ranked, so if it isn't listed, go straight to step 2 with the Actor name.
  2. fetch-actor-details with actor ataism/web-stack-patch-checker returns the input schema, pricing and output schema (free).
  3. call-actor with actor = ataism/web-stack-patch-checker and input = {"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.
  4. get-dataset-items with that dataset ID returns the rows (follow the nextStep in the call-actor response).

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 os
from apify_client import ApifyClient
client = 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):

  1. Read the URL column with a Google Sheets node and combine it into one list (for example with an Aggregate node).
  2. Add the Apify node, operation Run an Actor and get dataset, Actor ataism/web-stack-patch-checker, input {"urls": <your list>}.
  3. Filter on riskTier = high or review and send the rows to your ticketing tool or back to the sheet, matching on url.

Make:

  1. 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.
  2. Add the Apify module Get Dataset Items and map the run's default dataset ID.
  3. Add a filter on riskTier and 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.