
How to Detect WordPress Database Malware
Most WordPress malware scanners only look at files. Wordfence, Sucuri, MalCare — they all scan your filesystem for known signatures and tampered code. They don't routinely scan the database. Which is a problem, because the database is exactly where the most resilient WordPress malware hides. This page covers what database malware looks like, the SQL queries that find it, and how to remove what you find without breaking your site.
Why file-based scanners miss database malware
File scanners work by reading every PHP file on disk and matching the contents against a signature database — known malware patterns, suspicious function calls, base64-encoded payloads. This catches the majority of file-based infections quickly and cheaply.
It doesn't catch anything stored in the database, because the database isn't a filesystem. The malware is stored as text inside a database column — usually `wp_options.option_value`, `wp_postmeta.meta_value`, or directly inside `wp_posts.post_content`. To the scanner reading files, the database is invisible.
Attackers know this, which is why database persistence is now standard practice. Three patterns you'll see most often: serialised PHP arrays containing executable code that gets unserialised by certain plugins; injected `<script>` tags or iframes inside post content that load attacker-controlled JavaScript on every page view; and rogue admin accounts with no obvious tampering on disk at all.
The good news: database malware is detectable with simple SQL queries. You don't need a special tool — just access to your database (via phpMyAdmin, Adminer, or `wp db query` from SSH) and the queries below.
The 4 tables attackers hijack
`wp_options` — site-wide settings. Loaded into memory on every WordPress page load. Anything stored here runs constantly. The most popular table for sophisticated attackers because the payload runs site-wide without needing a user to visit a specific page.
`wp_postmeta` — per-post metadata. Common target for WooCommerce attackers because product meta fields are loaded on shop pages. Also useful for SEO-spam attacks that inject hidden content into specific posts.
`wp_posts` — the actual post and page content. Direct injection of `<script>` tags, iframes, or hidden links happens here. Easier to detect because the content is human-readable, but easy to overlook because most sites have hundreds or thousands of posts.
`wp_users` — registered users. Attackers add a new admin account and use it weeks or months later. Sometimes they modify an existing user's role or password hash without adding a new row at all.
If your site uses a custom table prefix (anything other than `wp_`), substitute it in every query below. Find your prefix by opening `wp-config.php` and looking for the `$table_prefix` line.
SQL queries — wp_options
Query 1 — common malware signatures:
`SELECT option_name, LEFT(option_value, 200) AS preview FROM wp_options WHERE option_value LIKE '%base64_decode%' OR option_value LIKE '%eval(%' OR option_value LIKE '%gzinflate%' OR option_value LIKE '%str_rot13%' OR option_value LIKE '%phar://%';`
Returns rows containing the standard PHP obfuscation primitives. False positives are rare — most legitimate options don't contain executable PHP. Investigate every result.
Query 2 — suspiciously large autoloaded options:
`SELECT option_name, LENGTH(option_value) AS size, autoload FROM wp_options WHERE autoload = 'yes' AND LENGTH(option_value) > 100000 ORDER BY size DESC LIMIT 20;`
Autoloaded options are loaded on every request. Anything autoloaded and over 100KB is suspicious — either a poorly-written plugin or malware. Plugins occasionally do this badly, so investigate before deleting.
Query 3 — recently modified options without a known plugin source:
Most managed hosts log option changes; if yours doesn't, run the malware-signature query above weekly and diff against last week's results. New entries containing executable patterns are almost always malicious.
Removing safely: before deleting any wp_options row, copy it to a text file as a backup. Then `DELETE FROM wp_options WHERE option_name = 'the_suspicious_name';`. Refresh your site immediately — if anything breaks, restore the row from your backup.
SQL queries — wp_postmeta and wp_posts
Query 4 — script and iframe injection in post meta:
`SELECT post_id, meta_key, LEFT(meta_value, 200) AS preview FROM wp_postmeta WHERE meta_value LIKE '%<script%' OR meta_value LIKE '%<iframe%' OR meta_value LIKE '%base64_decode%';`
WooCommerce stores especially: attackers inject malicious JavaScript into product meta fields so it loads on every shop page view but invisibly to admins reviewing the products in the dashboard.
Query 5 — injected scripts in post content:
`SELECT ID, post_title, post_status FROM wp_posts WHERE post_content LIKE '%<iframe src="http%' OR post_content LIKE '%<script src="http%' OR post_content LIKE '%document.write%' OR post_content LIKE '%eval(unescape%';`
Some results are legitimate (an embed you added, a third-party widget). Open each post in the editor and verify. The malicious ones are usually inserted at the very top or very bottom of the content, often inside a hidden `<div style="display:none">`.
Query 6 — hidden spam pages:
`SELECT ID, post_title, post_type, post_date FROM wp_posts WHERE post_status = 'publish' AND (post_title LIKE '%viagra%' OR post_title LIKE '%casino%' OR post_title LIKE '%loan%' OR post_title LIKE '%pharma%') ORDER BY post_date DESC;`
Adjust the keywords for the spam category that hit your site. SEO-spam attacks publish hundreds of pages targeting prescription drugs, gambling, payday loans, or escort services. They're often `post_type = 'page'` and not linked from anywhere on your site, but Google indexes them anyway.
Removing safely: trash the posts first (`UPDATE wp_posts SET post_status = 'trash' WHERE ID = ...;`) instead of deleting outright. If your site breaks or analytics show real traffic to one of the "spam" URLs, you can restore from trash. After 24 hours of no issues, empty the trash.
SQL queries — wp_users
Query 7 — all administrators:
`SELECT u.ID, u.user_login, u.user_email, u.user_registered FROM wp_users u JOIN wp_usermeta um ON u.ID = um.user_id WHERE um.meta_key = 'wp_capabilities' AND um.meta_value LIKE '%administrator%';`
Adjust `wp_` to your table prefix in two places. The result should be a short list — every admin you personally created. Common attacker-added usernames: `wpadmin`, `support`, `admin2`, `wpsupport`, or your own first name with a small variation. Recently-registered admin accounts you don't recognise are almost always rogue.
Query 8 — users with admin capabilities granted via meta only:
Some attackers grant admin rights through usermeta without changing the user_role. Cross-check Query 7 against `wp user list --role=administrator` from WP-CLI. Discrepancies indicate tampered metadata.
Removing safely: never delete a user account directly via SQL — WordPress has cleanup logic for content reassignment. Use `wp user delete <id> --reassign=<your_id>` from WP-CLI to delete the rogue admin and reassign their content to your account. After that, immediately rotate your own admin password — if a rogue admin existed, your password may also be compromised.
How long this takes
Realistic time estimate for a full database malware audit:
Best case — 30 minutes. You're comfortable with SQL, you know what to ignore, you find nothing or one obvious thing on the first pass. Possible if you've done this before.
Typical case — 2 to 4 hours. You walk through every query, get a handful of results from each, investigate which are legitimate and which are malicious, decode any base64 payloads to understand what they do, then carefully remove the confirmed-malicious entries. This is what a thorough first-time audit looks like.
Worst case — 6 to 10 hours, often spread across multiple sessions. You find rogue admins, multiple injected payloads in wp_options, hundreds of SEO-spam posts to triage, and have to research each unfamiliar option name to determine if it's a plugin doing something weird or genuine malware. Common on sites that have been compromised for months without being noticed.
Add another 30 minutes for setting up phpMyAdmin or learning `wp db query` if you haven't used either before.
The toolkit below runs the wp_options, wp_postmeta, post-content, and orphan-table checks in under 2 minutes and presents the results in a single report. You still need to investigate what it finds — but the discovery phase, which is most of the manual time, is automated.
Removing what you find — without breaking the site
Database changes are harder to undo than file changes. A bad DELETE can take down your site permanently if you don't have a recent backup.
Always back up the database first. From SSH: `wp db export ~/db-before-cleanup-$(date +%Y%m%d).sql`. This file is your insurance — if anything you delete breaks the site, you can restore the entire database in minutes.
Test on a staging site if you have one. Most managed hosts (Kinsta, WP Engine, SiteGround) include staging. Apply the same DELETE statements there first, click through the site, confirm nothing breaks. Then apply to production.
Delete in small batches. If you have 200 spam posts to remove, don't `DELETE FROM wp_posts WHERE post_title LIKE '%viagra%';` in one shot. Trash them in batches of 20, click through the site after each batch, and only empty the trash once you're sure the site still works.
Refresh the visitor-facing site after every change. Open an incognito window (so caching plugins don't hide problems from you) and load the homepage and a few key pages. If something breaks, restore from your backup immediately — don't try to fix forward.
Re-run the queries 24 hours later. If a sleeper still exists somewhere, the malicious entries will reappear. Finding the same row again means the entry point isn't closed yet — go back to file-system checks for the backdoor that's re-creating them.
Related fix guides
Vulnerable Plugins Detected
One or more WordPress plugins has known security vulnerabilities. Learn how to find and update them.
User Enumeration
WordPress is exposing your admin usernames via its REST API. Block it with one code snippet.
Login Page Exposed
Your WordPress login page is publicly accessible. Learn how to protect it from brute-force attacks.
Sensitive Files Publicly Accessible
WordPress ships with files that reveal your site version. Learn how to block public access in minutes.
GuardingWP checks your site for the 11 most common WordPress vulnerabilities — plus scans your installed plugins against the known CVE database. Free, no account required.
Scan for the entry points that let database malware in →Stop writing SQL queries by hand
Database malware hides in wp_options, post_meta, user_meta, and obfuscated post content. The Forensic Toolkit dumps every suspicious row — base64 strings, eval() patterns, unknown admin users, malicious cron entries — without you writing a single SELECT. One zip, run on your server, full DB anomaly report.
Get the Forensic Toolkit — from $25 →Prefer to have this handled for you? Get this fixed — Full Hardening ($149) →