PHP2Shell (CVE-2026-109618, CVE-2026-109619): critical RCE, exploited in the wild. Fixed in 8.5.12 · 8.4.27 · 8.3.36 · 8.2.35. Read the advisory
CLI documentation

The shellproof command line

Find and fix PHP security exposure from your own shell. One file, one dependency-free Python standard library, no account. It inspects the machine it runs on and the paths you give it, and sends nothing about them anywhere.

Every command and sample on this page was produced by running shellproof 1.1.0. Vulnerability data comes from php.net.

Install

Shellproof needs Python 3.8 or newer and nothing else — no pip packages, no compiler, no network for installation. It is tested on CPython 3.8 through 3.13. Pick whichever path fits the box you are triaging.

Zero install (recommended for incident response)

Download the single-file application and run it with the Python already on the host. The .pyz is a standard zipapp that bundles the whole package and its vulnerability database.

shell
curl -LO https://shellproof.dev/downloads/shellproof.pyz
python3 shellproof.pyz scan

The file is also linked directly: shellproof.pyz. Each version is attached to its GitHub release too. Every subcommand works the same way — python3 shellproof.pyz version 8.3.12, python3 shellproof.pyz fix, and so on. Throughout these docs the command is written as patched; substitute python3 shellproof.pyz if you are running the bundle.

One-liner triage with quickcheck.sh

For a 3am first look, quickcheck.sh is POSIX shell with no dependencies. It only reads php -v for every PHP binary on PATH and tells you whether each is affected by PHP2Shell. It does not inspect configuration, dependencies, code or running workers — use the full scan for that.

shell
curl -LsS https://shellproof.dev/downloads/quickcheck.sh | sh
Piping a script into sh runs it sightunseen.

Prefer curl -LO then read it before running — it is short and readable. The same caution applies to any install one-liner, ours included.

Real output on a host with no PHP on PATH (exit code 0; a web server may still bundle its own PHP via FPM or mod_php):

sh quickcheck.sh
Shellproof quickcheck — PHP2Shell (CVE-2026-109618 / CVE-2026-109619)
Scanning PHP binaries on PATH...


No PHP binary found on PATH. A web server may still bundle its own PHP
(FPM, mod_php, LiteSpeed lsphp). Check the version your site reports and run the
full tool to inspect it: python3 shellproof.pyz scan

When it finds an affected binary it exits 1; an end-of-life branch exits 1 with a migration note.

pip / pipx from source

To get a real shellproof command on PATH, install the package. pyproject.toml declares the shellproof entry point, so pipx gives you an isolated executable:

shell
# isolated executable (recommended)
pipx install git+https://github.com/nicolascoculuzzi/shellproof

# or into the current environment
pip install git+https://github.com/nicolascoculuzzi/shellproof

# or from a checkout
git clone https://github.com/nicolascoculuzzi/shellproof
pip install ./shellproof

After either, shellproof --version prints shellproof 1.1.0. The distribution name on an index is shellproof; the command it installs is shellproof.

Quickstart

Three commands cover most of a response: check one version string, scan a tree, and get the fix steps for this host.

1. Check a single version

shellproof version takes any version string — 8.3.12, the first line of php -v, or an X-Powered-By header value — and prints a verdict:

2. Scan a webroot

Point scan at a path (it defaults to the current directory). With no flags it also inspects the host's PHP binaries, /etc configuration and running workers; add --no-system to scan only the files you name. Output is grouped by category and coloured on a TTY. Trimmed real run against a small sample tree (--no-system --offline):

shellproof scan ./example.com --no-system --offline
shellproof 1.1.0  (data: php.net, generated 2026-10-22T06:22:24Z)
scanned: ./example.com
findings: 2 critical, 2 high, 2 low, 1 info

== Application code (1) ==
[HIGH] CODE-EVAL  Use of eval()
    example.com/public/up.php:1
    php.net: the eval() construct 'is very dangerous because it allows
    execution of arbitrary PHP code. Its use thus is discouraged.' ...
    evidence: <?php @eval($_POST["x"]);
    fix:
      Remove eval(). Replace dynamic dispatch with an explicit allowlist ...
    ref: https://www.php.net/manual/en/function.eval.php

== Indicators of compromise (3) ==
[CRITICAL] COMPROMISE-EXEC-SUPERGLOBAL  Command or code executed directly from request input
    example.com/public/up.php:1
    A command/code-execution sink (system, exec, shell_exec, passthru,
    popen, proc_open, pcntl_exec, eval or assert) receives a value taken
    straight from $_GET/$_POST/$_REQUEST/$_COOKIE/$_SERVER ...
    evidence: <?php @eval($_POST["x"]);
    fix:
      Treat this host as compromised. Isolate it, preserve the file for
      forensics, remove it or restore from a known-good backup ...

== File integrity (2) ==
[CRITICAL] INT-ENV-EXPOSED  Environment file served from a public web root
    example.com/public/.env
    public/.env sits inside the public web root 'public'. If the web server
    serves it, every secret it holds ... is downloadable.

... (3 more findings: INT-APP-DEBUG, 2x COMPROMISE-RECENT-MODIFICATION, DEP-OFFLINE-SKIPPED)

RESULT: findings at or above high. (exit 1)
Reading the result.

The header line gives the counts by severity; each finding carries a stable rule id, a location, masked evidence, a concrete fix and sources. The final RESULT line and the exit code reflect --fail-on (default high): 0 = nothing at or above the threshold, 1 = findings at or above it, 2 = a usage or runtime error.

3. Get the fix steps

shellproof fix detects this host's platform, PHP branch and SAPI and prints the exact upgrade, restart and verify commands, the interim mitigations and the after-patch checklist. The output never runs anything; --format sh -o fix.sh writes a script for you to review and run yourself.

shell
shellproof fix                       # detect this host and print the playbook
shellproof fix --platform ubuntu-ondrej --branch 8.3 --sapi fpm-fcgi
shellproof fix --format sh -o fix.sh  # a reviewable script; NOT executed

shellproof scan

The full scan runs six checks in order: runtime (PHP binaries, versions, stale workers), config (php.ini, FPM pools, web-server config), deps (composer.lock against Packagist advisories), code (risky code and webshell signatures), crypto (exposed keys, seed phrases, drainers) and integrity (WordPress core checksums, exposed .env and backups).

usage
shellproof scan [PATH ...]            # PATH defaults to the current directory

Flags

FlagEffect
--php BINInspect this PHP binary instead of auto-discovery. Repeatable.
--ini FILEAudit this extra php.ini / FPM pool file. Repeatable.
--no-systemDo not inspect host binaries, /etc configs or processes — a files-only scan of the paths you give.
--skip CHECKSkip a check: runtime, config, deps, code, crypto or integrity. Repeatable.
--offlineNo network: skip the Packagist advisory API and WordPress checksums.
--since YYYY-MM-DDFlag files modified on or after this date. Default 2026-10-20.
--exclude GLOBExclude paths matching GLOB. Repeatable.
--max-file-size MBSkip content checks on files larger than MB. Default 2.
--format FMTtext, json, sarif, html or md. Default text; inferred from the -o extension when omitted.
-o, --output FILEWrite the report to FILE instead of stdout.
--fail-on SEVExit 1 if any finding is at or above SEV. One of info, low, medium, high, critical, or never. Default high.
-q, --quietPrint only the summary header and the result line.
--no-colorDisable ANSI colour (also honours NO_COLOR).
-v, --verboseLog progress to stderr.

Exit codes

CodeMeaning
0Nothing at or above --fail-on (or --fail-on never).
1At least one finding at or above --fail-on.
2Usage error, or the scan could not run.

So --fail-on is what makes scan useful as a gate. --fail-on never always exits 0, which is handy when you want the report but not a failing step:

shellproof scan ./example.com --no-system --offline --fail-on never -q
shellproof 1.1.0  (data: php.net, generated 2026-10-22T04:58:35Z)
scanned: ./example.com
findings: 2 critical, 2 high, 2 low, 1 info

RESULT: nothing at or above never.

All commands

Run shellproof <command> --help for the exact flags of any subcommand.

version

usage
shellproof version VERSION [--windows] [--json] [--no-color]

Verdict for one PHP version string. --windows evaluates the host as Windows (so the php-cgi Best-Fit advisory applies). --json emits the full verdict — status, missing_security_releases, applicable cves and advisories, upgrade_to, recommended_branches — for scripting:

shellproof version 8.3.12 --json (abridged)
{
  "input": "8.3.12",
  "version": "8.3.12",
  "branch": "8.3",
  "known": true,
  "branch_status": "security",
  "branch_security_until": "2027-12-31",
  "latest_in_branch": "8.3.36",
  "is_latest": false,
  "missing_security_releases": ["8.3.14", "8.3.19", "...", "8.3.36"],
  "advisories": [
    {"id": "php2shell-oom", "cve": "CVE-2026-109618", "name": "PHP2Shell",
     "severity": "critical", "exploited_in_wild": true,
     "fixed_in": "8.3.36", "status": "vulnerable"}
  ],
  "status": "critical",
  "headline": "PHP 8.3.12 is affected by a critical, actively exploited vulnerability. Upgrade to 8.3.36 now.",
  "upgrade_to": "8.3.36",
  "recommended_branches": ["8.5", "8.4"]
}

fix

usage
shellproof fix [--platform ID] [--branch X.Y] [--sapi SAPI] [--format text|md|sh] [-o FILE]

With no flags it detects the platform, branch and SAPI on this host. It prints the upgrade, restart and verify commands with versions filled in from the live release data, the interim mitigations and the after-patch checklist. --format md is ticket-ready; --format sh -o fix.sh writes a reviewable script that Shellproof does not execute. Real excerpt (detected a Homebrew/macOS host):

shellproof fix
shellproof fix — Homebrew (macOS)
(auto-detected platform/branch/SAPI on this host; override with --platform/--branch/--sapi)
Target: PHP 8.4.27  ·  Homebrew (macOS)  ·  CLI / cron

== Upgrade ==
1. Update and upgrade the PHP formula
     $ brew update
     $ brew upgrade [email protected]
     $ brew link --overwrite --force [email protected]

== Verify ==
     $ php -v
     $ [email protected] -v || true
   The version a browser sees can differ from the CLI; confirm every SAPI
   reports 8.4.27 or newer.

== After the patch ==
   * Restart every PHP worker and web server
   * Rotate secrets that may have leaked
   * Scan for signs of compromise
       $ shellproof scan --since 2026-10-20
   ...

See the interactive fix generator for every platform.

monitor

usage
shellproof monitor [PATH ...] [scan flags] [--interval DURATION] [--once] [--state FILE]
                [--webhook URL] [--webhook-format json|slack|discord]

Re-scans on an interval and reports what changed. Covered in Monitoring below.

wallet

usage
shellproof wallet ADDRESS [--rpc URL] [--since YYYY-MM-DD] [--limit N] [--json]

Read-only Solana activity for one address, for checking whether a hot wallet moved funds after a key exposure. It validates the address (base58, 32 bytes) before calling, and only ever reads — it issues a getSignaturesForAddress JSON-RPC request and never signs or sends anything. The default endpoint is https://api.mainnet-beta.solana.com; override it with --rpc.

shellproof wallet So111…1112 --limit 2
Solana activity for So11111111111111111111111111111111111111112
  endpoint: https://api.mainnet-beta.solana.com
  2 transaction(s), newest first:
    2026-10-22 06:22:16Z  ok      3nAYqr2rzxjeQs2uukjyk1SJBTmKG66riPVcuMaK887Ce…
    2026-10-22 06:22:16Z  FAILED  QF5BefFWCerA2CxAj3nTJwGqiC7v6AG1R1SL4syafX8C5s…

An invalid address is rejected before any network call (exit 2): shellproof: address is not valid base58.

update

usage
shellproof update

Refreshes the vulnerability data from php.net into ~/.cache/shellproof. Every later command prefers this cache over the copy bundled in the tool, so you stay current between releases.

shellproof update
shellproof: fetching php.net release index for PHP 5
shellproof: fetching php.net ChangeLog-5
shellproof: fetching php.net release index for PHP 7
shellproof: fetching php.net ChangeLog-7
shellproof: fetching php.net release index for PHP 8
shellproof: fetching php.net ChangeLog-8
shellproof: refreshed vulnerability data from php.net
  wrote /Users/you/.cache/shellproof/php-releases.json
  generated_at: 2026-10-22T06:22:24Z
  530 releases, 18 branches

Report formats

scan renders the same findings in five shapes. Choose with --format, or let -o report.html infer it from the extension.

FormatFor
textReading in a terminal. Grouped by category, coloured on a TTY.
jsonScripting and pipelines. The canonical envelope below.
sarifGitHub code scanning. SARIF 2.1.0, one rule per finding id.
htmlA single self-contained file styled like this site, for sharing.
mdPasting into a ticket or pull request.

JSON envelope

Every JSON report has the same top-level shape. summary, findings, errors and installs are always present; this is the structure the website reads too.

shellproof scan … --format json (abridged)
{
  "tool": "shellproof",
  "version": "1.0.0",
  "generated_at": "2026-10-22T06:21:46Z",
  "data_generated_at": "2026-10-22T04:58:35Z",
  "target": ["./example.com"],
  "summary": {
    "total": 7,
    "by_severity": {"info": 1, "low": 2, "medium": 0, "high": 2, "critical": 2},
    "by_category": {"compromise": 3, "integrity": 2, "code": 1, "dependency": 1},
    "worst": "critical",
    "fail_on": "high"
  },
  "findings": [
    {
      "id": "COMPROMISE-EXEC-SUPERGLOBAL",
      "severity": "critical",
      "title": "Command or code executed directly from request input",
      "category": "compromise",
      "location": "example.com/public/up.php",
      "line": 1,
      "evidence": "<?php @eval($_POST[\"x\"]);",
      "fix": "Treat this host as compromised. Isolate it, preserve the file …",
      "references": ["https://www.php.net/manual/en/function.exec.php"],
      "confidence": "high",
      "fingerprint": "ca22005545e240ac"
    }
  ],
  "errors": [],
  "installs": []
}

fingerprint is stable across runs for the same finding, which is how monitor tells NEW from RESOLVED and how SARIF results dedupe in code scanning.

Continuous integration

The repository ships a composite GitHub Action (action.yml) that runs a scan, writes SARIF, uploads it to GitHub code scanning and fails the job on findings at or above your threshold. Findings then appear in the repository's Security → Code scanning tab, annotated on the pull request.

.github/workflows/shellproof.yml
name: Shellproof
on:
  push:
    branches: [main]
  pull_request:
  schedule:
    - cron: "0 7 * * 1"   # weekly re-scan catches newly disclosed CVEs

permissions:
  contents: read
  security-events: write  # required to upload SARIF

jobs:
  shellproof:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: nicolascoculuzzi/shellproof@v1
        with:
          path: .
          fail-on: high        # info|low|medium|high|critical|never
          # offline: "true"    # skip Packagist + WordPress checks
          # args: "--skip crypto --exclude 'tests/*'"

The action inputs map to scan flags: path, fail-on, offline, output (the SARIF file, default shellproof.sarif), args (anything extra passed straight through to shellproof scan) and upload-sarif (set false to keep the SARIF as a build artifact only). It always uploads the SARIF before enforcing fail-on, so results reach the Security tab even on a failing run.

Without the action

On any other CI, call the tool directly and hand the SARIF to the official uploader:

shell
python3 shellproof.pyz scan . --format sarif -o shellproof.sarif --fail-on high
GitHub: upload step
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: shellproof.sarif
    category: shellproof

Exit 1 fails the step when something is at or above --fail-on; exit 2 means the scan itself could not run.

Monitoring

shellproof monitor re-runs the same scan on an interval, diffs the findings against the previous run by fingerprint, and reports which are NEW and which are RESOLVED. It also refreshes php.net data each cycle and flags security releases it had not seen before — so a new PHP CVE shows up even if your files never changed.

It accepts every scan flag, plus:

FlagEffect
--interval DURATIONTime between cycles: 30m, 6h, 1d. Default 6h.
--onceRun a single cycle and exit. Use this under cron.
--state FILEWhere the baseline is stored. Default ~/.cache/shellproof/monitor-state.json.
--webhook URLPOST the delta to URL when anything changed.
--webhook-formatjson (raw delta), slack or discord. Default json.

First run against a tree establishes the baseline; everything is NEW and it exits per --fail-on. A later run with no changes reports nothing and exits 0:

shellproof monitor ./example.com --once --no-system --offline
[2026-10-22 02:22:01] shellproof monitor — 7 finding(s): 7 new, 0 resolved
  NEW:
    • [CRITICAL] COMPROMISE-EXEC-SUPERGLOBAL: Command or code executed directly from request input — example.com/public/up.php:1
    • [CRITICAL] INT-ENV-EXPOSED: Environment file served from a public web root — example.com/public/.env
    • [HIGH] CODE-EVAL: Use of eval() — example.com/public/up.php:1
    • [HIGH] INT-APP-DEBUG: APP_DEBUG is enabled in a production environment — example.com/public/.env
    ...
second run, nothing changed
[2026-10-22 02:22:01] shellproof monitor — 7 finding(s): 0 new, 0 resolved
  no change since last run

Under cron

Pair --once with your scheduler. A webhook is sent only when there is a delta, so a quiet host is quiet:

crontab
# every 6 hours: scan /var/www, notify Slack on any change
0 */6 * * * /usr/bin/python3 /opt/shellproof.pyz monitor /var/www --once \
  --webhook "$SLACK_WEBHOOK_URL" --webhook-format slack --fail-on never

Use --fail-on never under cron if you do not want a non-zero exit to be logged as a job failure; drop it if you do.

Privacy & network

Shellproof runs entirely on your machine. It reads files and runs local probes (php -v, ps, package-manager queries) to build findings, and nothing about the scanned host — no file contents, paths, versions, findings or identifiers — is ever uploaded anywhere. Detected secrets are masked before they reach a finding, a report, the console or any output.

The only outbound requests are read-only lookups of public vulnerability data, and they happen only for the commands that need them. All network I/O goes through one small module so it is easy to audit. --offline disables every one of them.

EndpointUsed byWhy
www.php.net/releases/index.php
www.php.net/ChangeLog-N.php
update, monitorFetch the PHP release index and changelogs to refresh the vulnerability database. Read-only; sends only the request.
packagist.org/api/security-advisories/scan (deps), monitorLook up advisories for the package names found in composer.lock. Only package names are sent — never your code, versions or paths.
api.wordpress.org/core/checksums/1.0/scan (integrity), monitorFetch official core checksums for a detected WordPress version to find modified core files. Only the version and locale are sent.
api.mainnet-beta.solana.com
(or your --rpc)
wallet, scan (crypto)Read-only getSignaturesForAddress for a Solana address you supply. Never signs or sends transactions.
your --webhook URLmonitorOnly if you configure it. Sends the finding delta (ids, severities, titles, locations) to a destination you choose.
That is the whole list.

Shellproof contacts no telemetry, analytics or update server of its own. Run with --offline and it uses only the data bundled in the tool — no connection leaves the host. You can read the one module that makes every request, shellproof/net.py, in the source browser.

Shellproof is defensive tooling: it helps you find and fix exposure on systems you operate. It is not a scanner for other people's hosts, it ships no exploit code, and it is independent and not affiliated with the PHP Group.