This file is generated from the rule registry in src/wp_malware_scan/rules.py.
Do not edit it by hand. Regenerate it with
python -m wp_malware_scan.ruledoc docs/rules.md.
Indicator values, such as attacker domains and option names, are deliberately
not listed here. They live in src/wp_malware_scan/iocs.py.
There are 36 rules. Each has a severity: CRIT, HIGH, MED, LOW or INFO. A rule marked as escalating means one finding from it, on its own, makes the verdict FULLY-COMPROMISED. Escalating rules cannot be allowlisted.
| Severity | Meaning | Effect on the verdict |
|---|---|---|
| CRIT | Malicious code or content, or something that is almost never harmless. | INFECTED, or FULLY-COMPROMISED when the rule escalates. |
| HIGH | Probably harmful, or a risky setting that a person must check. | SUSPICIOUS. |
| MED | Worth a manual check. Often legitimate. | No change on its own. The report asks for a manual check. |
| LOW | Low risk, or a limit of the scan such as a value that could not be decoded. | No change on its own. |
| INFO | Context for the reviewer, such as counts and external domains. | No change. |
The verdict follows the worst finding that is not allowlisted. An escalating rule gives FULLY-COMPROMISED, otherwise any CRIT gives INFECTED and any HIGH gives SUSPICIOUS. The spam rules (DB-SPAM-040, DB-SPAM-041 and DB-SPAM-042) give SPAM-ONLY when nothing worse was found. With none of these the verdict is CLEAN, which is never proof that a site is clean.
| Id | Severity | Escalates | Title |
|---|---|---|---|
| DB-IOC-001 | CRIT | yes | Known attacker domain in live content |
| DB-IOC-002 | HIGH | no | Cookie-stealer script marker in live content |
| DB-IOC-003 | INFO | no | Indicator only in security plugin log tables |
| DB-OPT-010 | CRIT | yes | Known backdoor option name |
| DB-OPT-011 | CRIT | yes | Option value decodes to PHP with dangerous calls |
| DB-OPT-012 | CRIT | no | PHP malware signature in an option value |
| DB-OPT-013 | HIGH | no | Suspicious option name shape |
| DB-OPT-014 | HIGH | no | Inline execution or decode call in an option value |
| DB-OPT-015 | CRIT | yes | Option looks like harvested credentials |
| DB-OPT-016 | LOW | no | Encoded option value that could not be decoded |
| DB-POST-020 | CRIT | no | Script injected into posts or post meta |
| DB-USR-030 | INFO | no | User and administrator counts |
| DB-USR-031 | MED | no | Unusually many users |
| DB-SPAM-040 | HIGH | no | SEO spam in published posts |
| DB-SPAM-041 | LOW | no | Spam in form-submission tables |
| DB-SPAM-042 | MED | no | Spam text in other tables |
| DB-DOM-050 | INFO | no | External domains to review |
| DB-TRG-050 | HIGH | no | Trigger, procedure, function or event in the database |
| FS-UPL-100 | CRIT | no | Executable PHP in uploads |
| FS-UPL-101 | CRIT | no | PHP hidden inside a non-PHP file |
| FS-MU-110 | HIGH | no | Must-use plugin present |
| FS-DROP-111 | MED | no | Drop-in present |
| FS-SIG-120 | CRIT | yes | Webshell signature |
| FS-SIG-121 | HIGH | no | Obfuscated dynamic execution |
| FS-IOC-130 | CRIT | yes | Known attacker domain in a file |
| FS-IOC-131 | CRIT | yes | Anti-scanner evasion |
| FS-IOC-132 | CRIT | no | Fake plugin header or slug |
| FS-CFG-140 | CRIT | yes | Dangerous code after the settings include in wp-config.php |
| FS-CFG-141 | HIGH | no | Content before the PHP tag in wp-config.php |
| FS-CFG-142 | HIGH | no | Statements after the settings include in wp-config.php |
| FS-HTA-150 | CRIT | no | Traffic-redirect marker in server configuration |
| FS-HTA-151 | HIGH | no | Prepend or append file hijack |
| FS-HTA-152 | MED | no | External redirect rewrite rule |
| FS-HTA-153 | CRIT | no | PHP handler mapped onto non-PHP files |
| FS-ROOT-160 | MED | no | Non-stock PHP file at the web root |
| FS-READ-170 | MED | no | PHP file could not be read |
- Severity: CRIT
- Escalates: yes, a single finding makes the verdict FULLY-COMPROMISED
- Matches: A domain from the list of known attacker domains appears in a live database table.
- Why it matters: Content that loads from or reports to that infrastructure means the site is running attacker code, and false positives are very unlikely apart from a security plugin quoting the domain, which DB-IOC-003 covers.
- Severity: HIGH
- Escalates: no
- Matches: Live database content contains a JavaScript marker used by a known cookie-stealing script.
- Why it matters: Stolen session cookies let an attacker take over administrator accounts, but some markers are ordinary browser calls, so a legitimate analytics snippet can match and the surrounding code needs a look.
- Severity: INFO
- Escalates: no
- Matches: A known indicator appears only inside the log tables of a security plugin and nowhere in live content.
- Why it matters: The plugin is reporting a detection rather than carrying live malware, so this is informational, but it is worth confirming that the detected item was cleaned.
- Severity: CRIT
- Escalates: yes, a single finding makes the verdict FULLY-COMPROMISED
- Matches: An options row uses a name, or a name pattern, known from backdoor frameworks that keep their payloads in the options table.
- Why it matters: Rows like this survive file cleanups and let the attacker return, and legitimate plugins are not known to use these names.
- Severity: CRIT
- Escalates: yes, a single finding makes the verdict FULLY-COMPROMISED
- Matches: An option value decodes through base64, single-byte XOR or layered encodings, up to three layers deep, into PHP that contains dangerous function calls.
- Why it matters: Code stored in the database and loaded at runtime is the signature of a self-healing backdoor, and the usual false positive, a code-snippet plugin, stores plain PHP rather than encoded PHP.
- Severity: CRIT
- Escalates: no
- Matches: An option value contains PHP that matches a high-confidence malware signature, such as eval of a decoded payload or a command run from request input.
- Why it matters: Backdoors fetch option values and run them, and the usual false positive is a code-snippet plugin storing raw PHP, which is why a bare PHP tag alone does not match.
- Severity: HIGH
- Escalates: no
- Matches: An option name ends in a long random hex suffix, or sits under a fake core name with a hex suffix, and does not start with a known legitimate prefix.
- Why it matters: Attackers hide rows behind names that look like core data, and the usual false positive is a plugin cache with a hashed name, which the prefix exclusions are meant to absorb.
- Severity: HIGH
- Escalates: no
- Matches: An option value contains an inline call to eval, base64_decode or gzinflate.
- Why it matters: Option values should be data rather than code, though some plugins and themes store scripts that mention these calls, so check what the value actually does.
- Severity: CRIT
- Escalates: yes, a single finding makes the verdict FULLY-COMPROMISED
- Matches: An option whose name suggests session or activity data holds several lines in a login and password shape.
- Why it matters: Backdoors harvest administrator credentials into the database for later collection, so every account on the site must be treated as exposed; the report gives the location and line count and never prints the values.
- Severity: LOW
- Escalates: no
- Matches: An option value, or a string embedded in it, carries the XOR prefix but cannot be deciphered, is a long base64 or hex blob that no supported codec turns into text, or needs more encoding layers than the decoder follows.
- Why it matters: A payload that resists decoding is invisible to the other option rules, so it needs a manual look, but large encoded options such as cached images and fonts are common and harmless, which is why this is a low severity pointer and values that decode to an image or font are left out.
- Severity: CRIT
- Escalates: no
- Matches: A post or post meta value holds a script block close to a known indicator, an eval call or a beacon call.
- Why it matters: Injected scripts steal cookies or redirect visitors, sometimes only for logged-in administrators, and the usual false positive is a legitimate embed or analytics snippet, which is why a script alone does not match.
- Severity: INFO
- Escalates: no
- Matches: Reports how many users exist and how many hold the administrator role.
- Why it matters: An unexpected administrator is a common persistence trick, so compare the count with the people who should have access; this is informational and never changes the verdict.
- Severity: MED
- Escalates: no
- Matches: The users table holds far more accounts than a typical site needs.
- Why it matters: Mass signups can be spam or a staging ground for abuse, though membership and shop sites legitimately have many users, so check the registration settings.
- Severity: HIGH
- Escalates: no
- Matches: Published posts contain terms and link markup typical of search engine spam.
- Why it matters: Spam pages damage search rankings and often come with deeper access, and the usual false positive is a post that quotes or discusses spam.
- Severity: LOW
- Escalates: no
- Matches: Spam text appears in tables that store form submissions.
- Why it matters: Contact form spam comes from the public, is not shown to visitors and does not harm the site itself, so it only calls for inbox hygiene.
- Severity: MED
- Escalates: no
- Matches: Spam text appears in tables that are neither posts nor form submissions.
- Why it matters: Spam in comments, options or custom tables can be shown to visitors or point to wider tampering, and the usual benign case is comment spam waiting in moderation.
- Severity: INFO
- Escalates: no
- Matches: Lists the external domains that appear most often in the dump and are not on the benign list, leaving out the site's own domain.
- Why it matters: Unfamiliar domains are a quick way to spot injected resources, but most will be ordinary services the site uses, so this never changes the verdict.
- Severity: HIGH
- Escalates: no
- Matches: The dump defines a trigger, a stored procedure, a stored function or a scheduled event. Each kind is reported with the object name, and the usual indicator and script checks also run over its text.
- Why it matters: WordPress core and common plugins never create these objects, yet a database trigger can rewrite content or re-add a backdoor each time a table changes, even after the files are cleaned, and nothing else in the scan looks at them. Some hosts and tools do add them, so this asks for a manual check. The logic inside is not analysed.
- Severity: CRIT
- Escalates: no
- Matches: A PHP file sits under the uploads directory, apart from stubs, data files and template caches that known frameworks and plugins write there and whose content has been checked.
- Why it matters: The uploads folder should hold media only, so PHP there is almost always a planted webshell, and the few frameworks and plugins that write harmless stubs, data files or compiled templates there are excused only when the content checks out. Any other plugin that does this is reported and can be allowlisted by prefix.
FS-UPL-101: PHP hidden inside a non-PHP file
- Severity: CRIT
- Escalates: no
- Matches: A file under uploads with a non-PHP name, including a double extension, contains a PHP opening tag.
- Why it matters: Attackers disguise code as images to pass upload filters and run it through another flaw, and a text file that merely shows PHP source is a rare false positive.
- Severity: HIGH
- Escalates: no
- Matches: A file exists in the must-use plugins directory.
- Why it matters: Must-use plugins load automatically and stay out of the normal plugin list, which makes them a common hiding place, though hosting providers and agencies add legitimate ones that should be checked against their source.
- Severity: MED
- Escalates: no
- Matches: A WordPress drop-in file, such as an object cache or advanced cache loader, exists in the content directory.
- Why it matters: Drop-ins replace core behavior and run on every request, so a malicious one is powerful, while caching and multisite setups use them legitimately and the content simply needs verifying.
- Severity: CRIT
- Escalates: yes, a single finding makes the verdict FULLY-COMPROMISED
- Matches: A PHP file runs eval or an operating system command taken from request input, rewrites itself, or writes PHP code taken from request input.
- Why it matters: These behaviors hand an attacker remote control of the server, and legitimate code essentially never has this shape, so a match should be treated as a compromise.
- Severity: HIGH
- Escalates: no
- Matches: A PHP file combines obfuscation with dynamic execution, for example eval of a decoded payload, hex-escaped function calls, a replace function using the code evaluation modifier, or indirection through the globals array.
- Why it matters: Obfuscated execution hides intent and is typical of malware droppers, though some commercial plugins obfuscate licensed code, so check the vendor before dismissing it.
- Severity: CRIT
- Escalates: yes, a single finding makes the verdict FULLY-COMPROMISED
- Matches: A file contains a domain from the list of known attacker domains.
- Why it matters: Code that contacts that infrastructure is part of an active compromise, and the only usual false positive is a security tool storing a signature list that mentions the domain.
- Severity: CRIT
- Escalates: yes, a single finding makes the verdict FULLY-COMPROMISED
- Matches: A PHP file checks the visitor's user agent against a list of three or more scanner names that includes the name of a security service.
- Why it matters: Malware does this to show clean pages to scanners and bad pages to everyone else, whereas security plugins mention scanner names without checking them as a list against the user agent, and a firewall that blocks attack tools lists only those tools.
- Severity: CRIT
- Escalates: no
- Matches: A file carries a plugin header or slug known from fake plugins that attackers use to pass as core or session handling code.
- Why it matters: A fake plugin lets the attacker persist in plain sight in the plugin list, and a name that merely resembles a real plugin can match by coincidence, so confirm where the plugin came from.
- Severity: CRIT
- Escalates: yes, a single finding makes the verdict FULLY-COMPROMISED
- Matches: The wp-config.php file contains code after the line that loads the WordPress settings, and that code includes another file, evaluates or decodes text, calls a function on an expression, by variable or by constant, passes a callback as a string or variable, reads request input, writes files, starts a process, contacts the network, or produces output or a redirect.
- Why it matters: Attackers append loaders after that line, and a loader needs one of these capabilities. Ordinary statements such as a define call or a host snippet use none of them and are reported as FS-CFG-142 instead, while comments and closing tags are not code and do not match. Output and redirects count because a loader can print a payload or send visitors elsewhere with no other capability.
- Severity: HIGH
- Escalates: no
- Matches: The wp-config.php file has visible content before the PHP opening tag.
- Why it matters: Injected content ahead of the tag runs or prints before WordPress loads, and the usual false positive is a byte order mark or stray editor artifact.
- Severity: HIGH
- Escalates: no
- Matches: The wp-config.php file contains statements after the line that loads the WordPress settings, and none of them uses a dangerous capability.
- Why it matters: This is normal on some host images and after manual edits, so review it once and allowlist it. Anything dangerous in the same place is reported as FS-CFG-140 instead and cannot be allowlisted.
- Severity: CRIT
- Escalates: no
- Matches: An .htaccess or .user.ini file contains a marker known from traffic-redirect and cache-bypass malware.
- Why it matters: The marker shows that server rules were rewritten to redirect or cloak visitors, and the rare false positive is a caching setup that uses a similar name, so review the rule itself.
- Severity: HIGH
- Escalates: no
- Matches: A server or PHP configuration file sets auto_prepend_file or auto_append_file.
- Why it matters: These directives run a chosen file on every PHP request, which attackers use for persistence, though a firewall plugin sets them legitimately, so its own loader is excused only when that loader sits at the site root with the expected shape and every other target must be confirmed.
- Severity: MED
- Escalates: no
- Matches: A rewrite rule in the server configuration redirects to an external site.
- Why it matters: Attackers send search visitors or mobile users elsewhere with such rules, but domain migrations and other legitimate redirects match too, so check the destination.
- Severity: CRIT
- Escalates: no
- Matches: An .htaccess file under uploads maps a PHP handler, or an .htaccess file anywhere maps a PHP handler onto a media or text file extension.
- Why it matters: Making an image or a text file run as PHP lets an attacker upload code under a harmless name, and an ordinary host line that maps only PHP extensions to the PHP handler does not match.
- Severity: MED
- Escalates: no
- Matches: A PHP file at the site root is not part of a standard WordPress install.
- Why it matters: Planted scripts often sit beside core files, and the usual benign cases are site verification files, host tools and custom entry points.
- Severity: MED
- Escalates: no
- Matches: A PHP file could not be opened or read during the scan, or it was larger than the read limit and only its head and tail were scanned.
- Why it matters: An unreadable or partly read file is an unscanned file and permissions or a very large file can be used to hide a payload, though broken permissions on a restored backup are the usual cause, so inspect the file manually.