Black Friday: 79% off every plugin Shop the sale →

Feature Overview

The update gate#

The gate hooks into WordPress at the moment a new package has been unpacked but before anything is installed. It then:

  1. Hashes every file of the new package and of the installed copy with SHA-256, and builds a package hash from the sorted list of paths and hashes. The hash is the same whether the package comes from the vendor or from quarantine.
  2. Classes files as added, removed or changed, with line diffs for text files up to the Diff size limit.
  3. Runs the rules engine on added and changed code only. PHP comments are stripped first so findings point at the right line.
  4. Scores the update and gives it a verdict: Clean, Review or Hold, based on the thresholds in Settings.
  5. On Hold, copies the package to quarantine, aborts the update with a clear error, and records a pending review. The plugin is left active and maintenance mode is turned off.

The gate works on single updates, bulk and AJAX updates from the Plugins screen, automatic background updates, WP-CLI updates, and “Replace current with uploaded” zip installs over an existing in-scope plugin. A package hash that is already pending or rejected is held again immediately without re-scanning, and a newer held package for the same plugin supersedes the older one.

Rules#

Each rule adds its weight once per package, however many places it matches, so one large library cannot inflate the score. Every match is still listed with its file, line and snippet.

RuleWeightDetects
remote-raw-ip40Calls to a bare IP address
remote-new-host15Network calls to a host the installed version never used
plugin-install40Code that installs plugins or writes into the plugins folders
plugin-activate30Code that activates plugins or edits the active plugin list
plugin-hide-list50Filters that hide plugins from the Plugins screen
plugin-hide-actions30Filters that remove plugin action links
admin-user-create50Creating administrators or super admins, direct capability writes
caps-grant25add_cap, add_role, set_role, capability filters
obf-eval50PHP eval()
obf-js-eval25JavaScript eval() / new Function()
obf-assert40assert() with dynamic input
obf-create-function40create_function()
obf-decode-chain40Nested decoders and function names built from string pieces
obf-long-string201,000+ character encoded strings, long escape runs or chr() chains
obf-preg-e50preg_replace() with the /e modifier
exec-shell30system, exec, shell_exec, passthru, proc_open, popen
cred-smtp40New references to SMTP settings or the PHPMailer password
cred-2fa40New references to two-factor secret storage
cred-auth-hook40Login and password hooks; reading passwords or 2FA codes from the request
every-request-remote30Hooks that run on every request combined with a network call
cloaking45Crawler user-agent checks combined with output changes or redirects
crawler-check20Crawler user-agent checks without output changes
php-in-unusual-dir30New PHP in assets/, images/, languages/ and similar folders
php-in-non-php-file50PHP open tags inside image, media, font or archive files
risky-file-type25.phtml, .phar, .php5, double extensions, .htaccess, .user.ini, web.config
server-config-exec45AddHandler, SetHandler, auto_prepend_file and similar in server config files
symlink30Symbolic links in the package
large-unscanned25Code files over the 8 MB scan size limit

With the default thresholds, one Hold-weight finding (50) or a few smaller ones together hold the update. Weights can be changed with the pug_rules filter.

Quarantine#

Held packages are stored as one zip per review, never as loose PHP files in a web-accessible folder. The folder is protected with index.php, .htaccess and web.config, and every read and delete is validated against the known quarantine folders. Package code is never executed while it is inspected.

Hidden plugin detector#

Plugin headers are read straight from disk, so filters and caches cannot hide a plugin from the detector. The results are compared with what the Plugins screen shows, from two vantage points:

  • a scan, run daily by cron, on demand from the Alerts screen, and by wp pug scan-hidden;
  • a passive check every time an administrator opens the Plugins screen, which catches malware that hides itself only there.

Every mismatch raises a Critical alert: plugins hidden from the list, active plugins that are not listed (including active files without a plugin header), plugins force-loaded by a filter on the active plugins option, a tampered plugin list, and hidden must-use plugins and drop-ins.

Integrity watch#

A SHA-256 baseline of every file is kept for each in-scope plugin. It is taken after a gated install, after any successful upgrader run, the first time the daily check sees a plugin without one, and when you click Accept changes on an alert. A held or failed update never refreshes a baseline, so it cannot hide tampering.

The daily check re-hashes every baselined plugin. Files that changed without an update raise a High alert when PHP, JavaScript or server-config files changed, and a Medium alert when only other files changed. .log files are ignored; add more patterns with the pug_integrity_ignore filter.

Fail mode#

If the gate itself fails (for example the package cannot be read or the quarantine folder is not writable), the If the gate fails setting decides what happens. Fail open (default) lets the update through, records the run as an error and raises a High alert. Fail closed stops the update, records it as blocked and raises a High alert.

Multisite#

On a network the plugin is network-activated, all its screens including License live in the Network Admin, and its data (reviews, alerts, baselines, settings) is network-wide. Every screen and action requires manage_options and manage_network_plugins.

Was this article helpful?