LW Scan
Lightweight malware scanner for WordPress: files, database and vulnerable software. It reports only and never modifies your site.
Overview
| Requires WordPress | 6.6+ |
| Requires PHP | 8.0+ |
| Tested up to | 7.1 |
| License | GPL-2.0-or-later |
| GitHub | lwplugins/lw-scan |
Quick Start
- Install and activate the plugin
- Open LW Plugins > Scan and press Start scan
- The first scan downloads the signature bundle and builds the file index, so it takes longer than the scans after it
- Review the findings; acknowledge, ignore or reopen them
Installation
Via Composer
composer require lwplugins/lw-scanManual Installation
- Download the release ZIP from GitHub Releases
- Install it through Plugins > Add New > Upload Plugin, or upload the
lw-scanfolder to/wp-content/plugins/ - Activate via the Plugins menu in WordPress admin
The release ZIP ships with its autoloader; a clone of the repository needs composer install before it will run.
Uninstallation
Deleting the plugin removes the settings, the run state, the three scan tables and the wp-content/lw-scan directory with the signature bundle and its caches. The cleanup covers the site it runs on, so a network install may leave per-site options behind.
What it scans
- Files: every file under the WordPress root is matched against the signature pack (a hash layer for known-bad files, a literal layer and a regex layer). The content type comes from the file itself, not from its extension, so a shell named
logo.pngis still scanned as PHP. - PHP heuristics (optional): a token-based layer that recognises obfuscation, dynamic code execution and disguised uploaders that no signature has seen yet. A heuristic hit is
suspicious, and only becomes an alert when the path it sits in argues for one (an executable underuploads, a PHP file disguised as an image). Every hit carries the reason it fired. - Integrity: core, plugins and themes are compared to the official wordpress.org checksums. Unmodified files are known-good and skip the expensive layers, which keeps the scan fast and keeps core files out of the findings list.
- Database: options, posts, post meta, user meta, users and database triggers.
- Vulnerable software: installed plugins, themes and the WordPress version itself, matched against a vulnerability feed with the affected version ranges resolved properly.
Findings are reports, never actions: LW Scan does not edit, quarantine or delete a file or a database row. The only things it writes are its own three tables and its own directory, wp-content/lw-scan.
Scheduling
Scans run on WP-Cron, hourly, daily or weekly, at an hour you choose (default: daily at 03:00).
- Catch-up: on a site whose cron never fires, the first ordinary page load past the due time schedules the run itself.
- Ticks: a scan advances in ticks that stay inside the request’s time budget. It can be stopped from the admin or the terminal and resumed where it left off. If PHP runs out of memory mid-tick, the run is reported as a failed scan naming the
memory_limitit hit.
Scopes: changed (the default, only what changed since the last scan), full, db (the database alone) and path (a single directory).
Notifications
Configure under LW Plugins > Scan > Notifications.
| Control | What it does |
|---|---|
| Send e-mail | Off, New alerts only or New alerts + review items. Off means no scan e-mail at all, the warning about repeatedly failing scheduled scans included. New installs start at Off. |
| Recipients | Comma-separated. Empty means the site’s admin address. |
| Maximum items per e-mail | 10, 20 (the default), 50 or All. |
| Admin notice | The persistent notice shown while new alerts exist. Independent of the e-mail switch. |
| Send a test e-mail | Mails the saved recipients and reports whether WordPress accepted the message. |
The first scan is a baseline. The first completed scan records its findings and sends nothing, because on a site that has been running for a while it reports everything already there. Every run after it mails what is new.
Status endpoint
A read-only status endpoint for external monitoring, on by default. The URL is on LW Plugins > Scan > Status:
GET /wp-json/lw-scan/v1/status/<key>The key in the path is the only credential, so treat the URL as a secret. A wrong key gets WordPress core’s own 404 rest_no_route answer, and while the endpoint is switched off the route does not exist.
| Query | Effect |
|---|---|
?http_status=1 | answers 503 instead of 200 while overall is crit |
?fresh=1 | runs the check now instead of reusing the stored result |
A computed result is reused for 5 minutes by default (1 to 60 minutes, set on the Status tab). The answer contains the site URL, the WordPress, PHP and LW Scan versions and the lw_scan check, whose status is ok, warn, crit or unknown. It never contains a filesystem path. Generate new URL replaces the key; the old URL stops working immediately.
A plugin that restricts the REST API to logged-in users (LW Disable’s Restrict REST API to logged-in users, for example) makes the endpoint answer 401 to a monitor.
WP-CLI
wp lw-scan run [<dir>] [--scope=<full|changed|db|path>] [--[no-]heuristics] [--resume]
wp lw-scan status
wp lw-scan findings [--severity=<alert|review>] [--type=<file|integrity|db|vulnerability>] [--state=<new|acknowledged|ignored>]
wp lw-scan ack <id>...
wp lw-scan ignore <id>...
wp lw-scan reopen <id>...
wp lw-scan bundle <status|update|force-full>
wp lw-scan index <rebuild|stats>
wp lw-scan stop
wp lw-scan endpoint <url|enable|disable|rotate|ttl|status>
wp lw-scan notify <status|enable|disable|level|recipients|limit|test>run carries the whole pipeline in one foreground process, and its exit code is what a cron job or a CI pipeline reads:
| Exit code | Meaning |
|---|---|
| 0 | the run finished and found nothing alerting |
| 1 | the run finished with at least one new alert-level finding |
| 2 | the scan could not start, or did not survive |
wp lw-scan run --scope=full
wp lw-scan run --scope=path wp-content/uploads
wp lw-scan findings --severity=alert --state=newAbilities
When the WordPress Abilities API is present, four abilities are registered in the lw-scan category, each behind a manage_options permission check: lw-scan/run, lw-scan/status, lw-scan/findings and lw-scan/acknowledge. This lets LW Site Manager’s MCP server, or any other Abilities client, drive the scanner.
Privacy
LW Scan sends only package slugs (and versions for checksums). No file contents, hashes, paths or site URL. Requests go to scan-data.lwplugins.com, operated by LW Plugins, for the signature bundle, the wordpress.org checksums and vulnerability records. Each request carries a User-Agent: lw-scan/<plugin version>; WordPress/<wp version> header. There is no telemetry, no account and no site identifier.
Requests happen on activation, while a scan runs, and when you press Check for updates or Re-download full bundle on the Health tab.
FAQ
Does it remove or repair what it finds?
No. LW Scan reports, and stops there. Deciding what to do with an infected file is yours to make.
Will it slow down my site?
Scans run in the background, in ticks that stay inside the request’s time budget, and the default scope only looks at files that changed since the last scan. On an ordinary page load the plugin reads nothing but options WordPress has already loaded, and runs no database query of its own.
Does it work on multisite?
The plugin activates and scans files on multisite, but the database scan covers the site the request runs on; sub-sites are not iterated.