Skip to content

How It Works

github-actions[bot] edited this page Nov 3, 2026 · 1 revision

How it works inside

The files

File Job
jat-shell-scanner.php Plugin header, constants, loads everything
includes/signatures.php All indicators — fixed PHP and WordPress versions, log patterns, hashes, IPs, code patterns. Easy to review and extend.
includes/class-scanner.php The scan engine: resumable job, locking, every check
includes/class-env.php Records the website's PHP (version, server type, php.ini settings, history)
includes/class-admin.php The admin page, AJAX scanning, host message, ignore list, notice
includes/class-cron.php Scheduled scans and email alerts
includes/class-settings.php Settings storage
includes/class-site-health.php Tools → Site Health test
includes/class-updater.php Updates from GitHub releases
includes/class-cli.php WP-CLI commands
assets/admin.js, assets/admin.css Progress bar, buttons (no jQuery, no external files)
uninstall.php Removes everything on delete
manifest.json (release zip only) SHA-256 fingerprints of every file, for the self-check
flowchart TB
    subgraph Triggers
        UI["Dashboard<br/>Scan now"]
        CRON["WP-Cron<br/>weekly / daily"]
        CLI["WP-CLI<br/>wp jat-shell scan"]
    end
    ENV["Website PHP snapshot<br/>class-env.php"]
    UI -->|"AJAX: start, then a step every few seconds"| ENGINE
    CRON -->|"start, then chained steps"| ENGINE
    CLI -->|"steps in a loop"| ENGINE
    UI -.->|"records on every admin page"| ENV
    subgraph ENGINE["Scan engine — class-scanner.php"]
        LOCK["Database lock<br/>one worker at a time"] --> JOB["Job state<br/>wp2ss_job option"]
        JOB --> PH["Phases: quick → files → core → plugins → logs"]
        SIG["signatures.php<br/>+ wp2ss_signatures filter"] --> PH
    end
    ENV --> PH
    PH --> REPORT["Report<br/>wp2ss_report option"]
    REPORT --> ADMIN["Admin page, host message, notice"]
    REPORT --> HEALTH["Site Health"]
    REPORT --> MAIL["Alert email<br/>scheduled scans only"]
    REPORT --> JSON["JSON / CLI output"]
Loading

A scan is a resumable job. Instead of one long request (which cheap hosts kill after 30 seconds), the scanner works for a few seconds, saves exactly where it is — down to the position inside a folder — and continues on the next request.

stateDiagram-v2
    [*] --> quick : start()
    quick : quick — PHP, WordPress, plugins, users, roles, site root, own files, drop-ins, database
    files : files — walk wp-content (and outside plugin/upload folders) depth-first
    core : core — compare core files with wordpress.org checksums
    plugins : plugins — compare wordpress.org plugins with their checksums
    logs : logs — PHP error log, debug.log, access log
    quick --> files
    files --> files : out of time, save place and resume
    files --> core
    core --> core : out of time, resume
    core --> plugins
    plugins --> plugins : out of time, resume
    plugins --> logs
    logs --> logs : out of time, resume at byte offset
    logs --> done
    done : done — sort findings, save report, fire wp2ss_scan_complete
    done --> [*]
Loading

One worker at a time. Each slice takes a short database lock. If two browser tabs — or a tab and a scheduled scan — try to work at once, the second waits its turn. A lock left by a crashed request expires after 5 minutes, and a cancelled scan can't be written back by a slice that was still running.

sequenceDiagram
    participant T1 as Browser tab 1
    participant T2 as Browser tab 2
    participant S as Scanner
    T1->>S: step
    S->>S: lock acquired - work, save
    T2->>S: step
    S-->>T2: busy, try again shortly
    S-->>T1: progress 40%
    T2->>S: step (1.5s later)
    S->>S: lock acquired - continue from saved place
    S-->>T2: progress 55%
Loading

Robustness built in

Situation What the scanner does
A check throws an unexpected error Reports it as an Info finding and carries on
A step could hang (e.g. a host silently dropping connections to wordpress.org) Attempted at most twice, then skipped with a note
A file can't be read Counted as unreadable; the scan moves on
A PHP file over 5 MB Hashed, and the first and last 1 MB checked (where injected code sits)
A folder with hundreds of thousands of files Read entry by entry; the scan can stop mid-folder; saved state stays tiny
Symlinked folders Followed once each (max 200); loops skipped
File names that aren't valid UTF-8 Stored safely and shown readably — they can't stall the scan
The scan pauses for hours (quiet site) Resumes; only a job with no progress for 24 hours is abandoned
Plugin updated mid-scan Old-format job is discarded; a fresh one starts
PHP warnings printed by the host Can't corrupt the progress bar's replies
A badly infected site At most 1,000 findings stored, plus a count of the rest
Database-only attacker Ignore list and admin baseline are signed with wp-config.php secrets; tampering is reported
Attacker edits this plugin Files checked against the release fingerprints published on GitHub (or WordPress.org's checksums for the WordPress.org edition)

Privacy and stored data

Nothing about your site is sent to us or any third party. The only outside requests are:

Request When Why
api.wordpress.org — core checksums During a scan, at most once a day (cached) To check core files are untouched. Turn off in settings.
downloads.wordpress.org — plugin checksums During a scan, once a day per plugin version (cached); only the plugin's folder name and version are sent To check plugin files are untouched. Same setting as core checksums.
downloads.wordpress.org — this plugin's checksums WordPress.org edition: at most weekly (cached) To check this plugin's own files weren't tampered with
api.github.com — latest release GitHub edition: at most twice a day To offer plugin updates
github.com — the release's manifest.json GitHub edition: at most weekly (cached) To check this plugin's own files weren't tampered with
github.com — release zip GitHub edition: only when you install an update The update itself
Your own hub site — fleet report After each scan, only if you set up fleet mode See the fleet dashboard for exactly what's sent
Your own hub site — check-in Hourly, only on fleet members Asks whether a new scan was requested; sends only the site address
Your Slack / Discord webhook Only on a hub with a webhook set Fleet alert digests

WP2SS_DISABLE_UPDATER turns off all three GitHub requests in the GitHub edition. The WordPress.org edition never contacts GitHub.

What it stores (all removed when you delete the plugin):

Name Where Contents
wp2ss_settings Options Your settings
wp2ss_report Options The latest report
wp2ss_job Options A scan in progress (deleted when it finishes)
wp2ss_ignored Options Findings you ignored (signed)
wp2ss_env Options The website's PHP version, server type, relevant php.ini settings and a short version history
wp2ss_admins Options Administrator user IDs at the last scan (signed)
wp2ss_fixes Options One-click fixes you applied, for undo (signed)
wp2ss_quarantine_folder Options Name of the random, locked quarantine folder in uploads/jat-shell-scanner/ (that folder and its files are deleted too)
wp2ss_quarantine_dir Options Builds before 1.6 only: the old quarantine folder in wp-content (deleted too)
wp2ss_fleet Options Fleet mode, hub address and key
wp2ss_fleet_sites Options (hub only) Latest report from each member site (up to 1,000)
wp2ss_fleet_status Options (members) When the last report was sent, and any error
wp2ss_fleet_pins Options (hub only) Each member's registered secret
wp2ss_fleet_queue Options (hub only) Alerts waiting to go out in the next digest
wp2ss_fleet_alerts, wp2ss_fleet_daily, wp2ss_fleet_poll WP-Cron Alert digest, daily overdue check (hub), hourly check-in (members)
wp2ss_lock Options (main site) Short-lived lock while a slice runs
wp2ss_release, wp2ss_core_*, wp2ss_manifest_* Transients Cached update info, checksums and fingerprints
wp2ss_dismissed User meta Which report's notice you dismissed
wp2ss_scheduled_scan, wp2ss_scan_step WP-Cron Scheduled scan events

On multisite these live in network-wide options.

Limits

  • PHP2Shell happens below WordPress. The plugin can judge your PHP version and settings and look for traces and leftovers, but it can't watch PHP's memory or processes, and it can't stop the attack. Only updating PHP does.
  • Memory leaks leave no trace on disk. If you ran a vulnerable PHP after October 17, 2026, change your secrets even if the scan is clean.
  • OS packages can be patched without a version change. The scanner reads Debian/Ubuntu package versions and PHP's build date, but on other systems it can't always tell. Your host can.
  • One-click fixes are a first aid kit, not a full clean-up. Quarantining what was found doesn't remove what wasn't; still change your secrets and scan again.
  • The fleet hub trusts each member about itself. A fully compromised member can send a fake "clean" report about itself (it can't touch other sites' rows). A site that stops reporting shows as overdue and triggers an alert.
  • Session-file, /tmp and /etc/ld.so.preload checks only work where PHP is allowed to look (many shared hosts block it with open_basedir); silence there isn't an all-clear.
  • Known indicators only. Attackers can rename files and rewrite code, so "no signs" means no known indicators — not a guarantee.
  • An attacker with full control of the server could tamper with any tool running on it, including this one. The self-check against GitHub or WordPress.org makes that harder, not impossible. For certainty, have your host check from outside.
  • It looks for these two attacks specifically. It isn't a general antivirus — keep your usual security plugin or host scanner.
  • Finding texts are in English; the interface is translation-ready (languages/jat-shell-scanner.pot, text domain jat-shell-scanner).

Clone this wiki locally