Elderward

Malware scanner and incident response for WordPress. Install the plugin, start one scan from the admin, and see what got into the site and how it got in, without changing a thing.

$ cd wp-content/plugins
$ git clone https://github.com/fioa8c/elderward.git elderward
$ wp plugin activate elderward

Free and GPL. Not on WordPress.org yet.

One scan, from the admin

Open Elderward in the admin menu and click Start scan. It runs in the background through WP-Cron, so you can close the tab, and it changes nothing on the site.

wp-admin › Elderward
ElderwardMalware scanner and incident response for WordPress
ScanSignaturesHardeningRecoveryAI assistants

Scan this site

  • WordPress, plugin and theme files compared with the official copies
  • Known malware in your files
  • Suspicious entries in the database and admin accounts
  • Files with faked dates, and the server crontab
  • Access logs in /var/log/apache2
Start scan

Last scan: complete. Started 2 Nov 2026 14:02, finished 14:09.

Findings

Export as text Export as JSON
AllHIGHMEDIUMLOWINFOFalse positives
SeverityTypeWhereDetailsActions
HIGHUPLOADS_PHPwp-content/uploads/2024/03/cache.phpexecutable PHP inside wp-content/uploads - usually a webshell dropped through an upload formMark as false positive
Download sample
Look up on VirusTotal
HIGHDB_OPTION_IOCdb:wp_options:widget_textdb.option.hidden-iframe: Hidden iframe inside an option value - a classic drive-by injectionMark as false positive
MEDIUMDB_NEW_ADMINdb:wp_users:ID=7administrator "wp-sync" <wp-sync@example.com> registered 2024-03-12 03:16:40 - created within the last 30 days, the classic takeover artifact; verify who created itMark as false positive
LOWINTEGRITY_MISSINGwp-admin/css/about.min.csscore 6.6.2: file from the official package is missingMark as false positive
The Elderward page in wp-admin: start a scan, then read the findings worst first.

What one scan looks at

Seven checks in one report. Anything that blocks a check, like an unreadable file or a missing PHP extension, is reported as an error instead of skipped.

Quick checksUPLOADS_PHP, PHP_PREPEND, MU_PLUGIN, HIDDEN_PHP
The cheap places attackers hide: executable PHP in wp-content/uploads, auto_prepend_file hooks in .user.ini, php.ini and .htaccess, mu-plugins that load with no activation step, and hidden dotfile PHP.
IntegrityINTEGRITY_MODIFIED, INTEGRITY_MISSING, INTEGRITY_EXTRA
Core, every plugin and every theme compared with the official wordpress.org package for the installed version. Extra PHP inside core or a plugin is the classic dropped webshell. Premium plugins and child themes that can't be checked are reported as such, so a clean report never quietly means unchecked.
DatabaseDB_NEW_ADMIN, DB_ROLE_CAPS, DB_OPTION_IOC, DB_POST_IOC
Read-only. Every administrator listed, with recent ones highlighted, non-admin roles granted takeover powers, PHP, hidden iframes or obfuscated JavaScript in options and posts, a hijacked siteurl or home, and tampered active_plugins.
Access logsLOG_IOC, LOG_FILE_CORRELATION
Optional: give it a log file or folder in Settings, rotated and .gz files included. Webshell requests, exploit attempts on known-vulnerable plugins, .env probes, code injection and brute-force floods, plus the files modified within minutes of that traffic.
Malware signaturesMALWARE_FOUND, SIGNATURE_MATCH
Known injections such as the ndsw/ndsx and wqpq JavaScript campaigns, plus any signatures you write. From the admin this runs as a dry run. Cleanup happens from the command line.
TimestampsTIMESTAMP_OUTLIER, TIMESTAMP_TIMESTOMP
Files changed after the rest of their package, and PHP whose modified time was pushed back with touch. These are leads, not proof.
CrontabCRONTAB_IOC
The crontab of the user PHP runs as, checked for piped base64 decoders, pastebin downloads and droppers in tmp directories.

Beyond the scan

The rest of the Elderward page, tab by tab.

Review findingsScan tab
Mark a finding as a false positive with a note. On a file, the mark holds only while the file is byte for byte the one you reviewed. Download sample packs a flagged file into a zip encrypted with the password infected, and file findings link to a VirusTotal lookup by hash. Export the report as text or NDJSON.
Scheduled scansScan tab, Settings
Daily, twice daily or weekly, in the site's timezone. If WP-Cron is unreliable on your host, run scans only while the page is open.
Custom signaturesSignatures tab
Exact text or regular expressions for files, option values or post content. Test one against a pasted sample or a file of the site, then share it as JSON with other sites and the command line. Custom signatures only detect.
HardeningHardening tab
Server rules that refuse PHP in uploads, copies of wp-config.php, hidden files, backups and logs, WordPress internals and folder listings. Start from the Recommended or Strict template, or pick each protection. On Apache and LiteSpeed it writes .htaccess and rolls back if the site stops responding. On Nginx you download a file to include. Check now tests what the live server actually refuses.
Recovery modeRecovery tab
A key-protected page that works when WordPress doesn't, after a white screen or a critical error. It shows the PHP errors that broke the site and the plugin or theme they point at, and runs the full scan. Without a key it answers 404.
AI assistantsAI assistants tab
On WordPress 6.9 or newer with the MCP Adapter plugin, Claude, ChatGPT, Antigravity or Cursor can run scans, explain findings, read flagged files and draft signatures with you. They can't clean or change files. No MCP? Copy a summary into any chat.

Wards it will not break

It runs on sites that are already compromised, so it treats every filename, log line and API response as hostile.

  • Scans change nothing

    Admin scans only report. The only files the admin ever writes are the .htaccess rules you apply and the recovery key you create.

  • Nothing leaves the server

    No account and no telemetry. The only outbound requests go to wordpress.org, for checksums and official packages.

  • TLS verification stays on

    An "all clean" answer must never come from an attacker sitting between the site and wordpress.org.

  • The database is only read

    SELECT queries only, wp-config.php is read without being executed, and stored values are never unserialized, which could trigger PHP object injection.

  • Blocked checks are loud

    A missing extension, an unreachable database or an unreadable log is an ERROR finding, never a silent skip. A clean report means every scanner actually looked.

  • A false positive can't hide a changed file

    A mark on a file stores its SHA-256. If the file changes, the finding comes back.

  • Assistants look, never touch

    No AI tool cleans, restores or writes files. Everything scanned is labelled as untrusted data, so a planted file can't give the assistant orders.

  • Restores are verified first

    Each file from the official zip is checked byte for byte against the reference checksums before it's written. A tampered package is refused, and extra files are never deleted.

  • Hardening undoes itself

    After writing rules, Elderward loads the home page, the login and an admin request. If any of them breaks, every file goes back exactly as it was.

Cleanup happens in a shell

Cleaning and restoring write files, so they run from WP-CLI, where you take a backup and preview first. Every scanner is a wp elderward command too, with output you can pipe.

investigator@host: /var/www/html

      

Clean and restore

$ wp elderward scan --dryrun
$ wp elderward scan
$ wp elderward integrity --restore --dryrun
$ wp elderward integrity --restore --quarantine-dir=/path/outside/webroot

Cleaned files are written to a temp file and renamed into place, so a failed write can't truncate the original. --quarantine-dir keeps each original as evidence.

When WordPress won't load

Open recovery mode, or run the same scanners from the plugin folder with the standalone CLI. It needs PHP 5.6 and no working WordPress.

$ php bin/elderward.php scanall=1 scandirectory=/path/to/wp-root
$ php bin/elderward.php help=1

Text

One line per finding, the same shape from every scanner. Paths and log content come from the compromised host, so control characters and commas in paths are percent-encoded. A crafted filename can't forge a finding line.

SEVERITY,TYPE,path,detail

JSON

--format=json prints one event per line on stdout and sends progress to stderr. The admin's JSON export uses the same events.

{"event":"finding","severity":"HIGH",
 "type":"LOG_IOC","path":"...","detail":"..."}

What the exit code tells you

  1. 0

    Clean, or informational findings only.

  2. 1

    Usage or fatal error. Check the arguments.

  3. 2

    LOW, MEDIUM or ERROR findings to review.

  4. 3

    HIGH findings. A successful cleanup exits 3 too: malware was found.

Install it

Elderward isn't on WordPress.org yet, so install it from GitHub.

Get the plugin

$ cd wp-content/plugins
$ git clone https://github.com/fioa8c/elderward.git elderward
$ wp plugin activate elderward

Or download the repository as a zip and unpack it as wp-content/plugins/elderward/. The folder has to be named elderward.

After activating

  1. Open Elderward in the admin menu, or the Network Admin on multisite, and click Start scan.
  2. Under Settings, add the location of the access logs and, if you want, automatic scans.
  3. Apply the Recommended template on the Hardening tab.
  4. Create a recovery key on the Recovery tab, so recovery mode is ready if the site ever breaks.
  5. Take a backup, then clean up with the WP-CLI commands shown under the results.

Requirements

  • WordPress 6.0 or newer and PHP 7.2 or newer. The standalone CLI and recovery mode run on PHP 5.6.
  • Shell access only to clean or restore files. The admin pages need none.
  • WordPress 6.9 and the MCP Adapter plugin for AI assistants.
  • Apache or LiteSpeed with AllowOverride for the hardening rules Elderward writes. On Nginx, someone with server access includes the generated file.
  • Some checks use PHP extensions that locked-down hosts lack: zlib for .gz logs, zip for themes and every restore, shell_exec for the crontab. A missing one is reported as an ERROR.

Take a backup before cleaning. Preview with --dryrun.

It finds known malware. A clean scan is not proof of a clean site, and log hits include plenty of attempts that failed.

Send what it missed

Elderward only knows malware someone has already caught. If you clean a site and find something it didn't flag, open a GitHub issue with a sample so the next scan catches it.

Put this in the issue

  • The sample. Download sample on the finding gives you a zip encrypted with the password infected, with a manifest of where and why it was flagged. GitHub won't take a bare .php attachment.
  • Its SHA-256 hash, and the path where you found it.
  • How it was loaded: an include in functions.php, an mu-plugin, a cron entry, auto_prepend_file, or something else.
  • The WordPress, plugin and theme versions, if you have them.
  • For attack traffic, the matching access log lines.
  • If you wrote a signature that catches it, export it from the Signatures tab and attach the JSON.

Package it from a shell

$ shasum -a 256 suspicious.php
$ zip -j -P infected sample.zip suspicious.php

Issues are public. Remove anything that identifies the site before you upload.

That means domain names, database credentials, salts, email addresses and visitor IPs in log lines. Never attach wp-config.php or a database dump.