Database Security Checks — What Each Finding Means
GuardPress Database Security runs 15 sub-checks across users, configuration, cleanup, malware patterns, and server version. Findings come back at four severity levels, and the right action depends on which sub-check fired. This is the reference for what each finding actually means, the action to take, and when a finding is safe to ignore. If you’re staring at a Critical finding for “Suspicious Post Content” or trying to decide whether 1,200 expired transients are a real problem, start here.
When the Scan Runs and Where Findings Show Up
Two ways the scan kicks off:
- Daily — via WordPress’s built-in cron, on the
guardpress_daily_scanhook. The exact time depends on when WP’s cron tick first fires on your site after the previous day’s scan; in practice it runs roughly every 24 hours. - On demand — click Run Scan Now on the Database Security admin page. The on-demand AJAX handler is registered regardless of whether the daily scan is enabled in settings, so the button always works.
Findings are stored in the guardpress_database_results WordPress option and surface in three places:
- The Database Security admin page itself — full list of findings, severity counts, and (for sub-checks that support it) one-click cleanup buttons.
- The audit log (GuardPress → Audit Log) — one entry per scan, action
database_scan_completed, summarising how many issues were found and how many were critical/high. - Security alert emails — the scan dispatches a notification via
GuardPress_Notifications::send_security_alert()when at least one finding is Critical or High severity. Low / Medium findings don’t trigger an email on their own.
Severity Ladder
Every finding is tagged with one of four severities. The cutoff for “you should be emailed about this” is Critical or High; Medium and Low are awareness-level.
- Critical — act today. Almost always a malware indicator (suspicious post content, tampered
active_plugins, poisoned capabilities, rogue cron events). - High — act this week. Strong security signal (default
adminusername, brand-new admin accounts, MySQL EOL, admin users with inconsistent capability metadata). - Medium — act this month. Hardening recommendation or performance gap (default
wp_table prefix, too many admin accounts, autoloaded options creeping into the 1–3MB range). - Low — informational. Database hygiene (orphaned meta, expired transients, post revisions, spam/trash, zombie cron events).
A Low “Orphaned Post Meta” finding is genuinely low for most sites. But a Critical “Suspicious Post Content” finding on a multi-author WP site that publishes code tutorials may be a false positive (legitimate code blocks containing base64_decode for instruction). Use severities as a triage order, not the final word.
The 15 Sub-Checks
Each scan runs every sub-check below in order. Sub-checks emit zero or more findings depending on what they detect. The list is canonical against GuardPress 1.6.37; sub-checks 12–15 (malware / tampering family) were introduced in 1.6.10.
Medium Default Table Prefix
Your WordPress tables use the default wp_ prefix.
Check
Reads $wpdb->prefix and compares it to the literal string wp_. Fires only when the prefix is exactly the default.
Why it matters
Mass-targeted SQL injection attacks and automated scanners assume table names like wp_users and wp_options. A non-default prefix breaks the assumption and turns precision attacks into trial-and-error.
Fix
Change the prefix at the install level (best done at WP install time). Mid-life prefix changes require renaming every table, updating wp-config.php’s $table_prefix, and updating two columns inside wp_options + wp_usermeta that reference the prefix in row data. Several plugins automate this safely — do a full backup first via SiteVault Pro.
Safe to ignore
When you weigh prefix change risk against the marginal protection gain, many small sites legitimately decide to skip this and rely on the firewall + brute-force layers instead. The finding is Medium, not High, for exactly this reason.
Medium Too Many Administrators
More than 5 user accounts hold the administrator role.
Check
SQL count of users whose wp_capabilities usermeta row contains the string administrator. Fires when the count exceeds 5.
Why it matters
Every admin account is a credential pair that, if compromised, gives an attacker total control. A practical security ceiling is one admin per actual human who needs full plugin/theme/user-management control; everyone else gets Editor, Author, or Shop Manager. Six or more admins almost always means stale staff accounts or contractors that never got demoted.
Fix
Go to WP Admin → Users → Administrators. Audit the list. Demote anyone who doesn’t need full admin to Editor or Shop Manager; delete anyone who shouldn’t have access at all.
Safe to ignore
If your team genuinely needs that many admins (small agency managing a client’s site with multiple stakeholders), you can leave it — the finding will keep appearing on every scan but doesn’t emit an alert email (it’s Medium, not High).
High Default Admin Username
A user account with the username admin exists.
Check
get_user_by('login', 'admin'). Fires if any user has that exact login.
Why it matters
admin is the first username brute-force scripts try, every time. It cuts an attacker’s required guess space in half: they now only need the password. If the account also has a weak password, this is a one-step compromise.
Fix
- Create a new admin user with a non-default username (your name, a randomised handle, anything but
admin/administrator/root/wp_admin) - Log in as the new user
- Delete the old
adminuser from Users, attributing their content to your new account
Safe to ignore
Almost never. The only edge case is a deeply locked-down install where the admin account has been moved off-network entirely and cannot log in via wp-login.php at all (deny rules in nginx/Apache). Even then, fixing it is faster than explaining it.
High Suspicious User Registrations
More than 10 new user accounts registered in the last 24 hours.
Check
SQL count of users with user_registered > NOW() - 24 hours. Fires when more than 10 new users appeared in the last day.
Why it matters
On a site that doesn’t actively run registration campaigns, ten new accounts in a day is a spam bot signal. Bots often register accounts to post comments / spam profile fields / drop SEO backlinks. On a WooCommerce store running a paid ad campaign, ten organic registrations a day is normal — context matters.
Fix
Go to WP Admin → Users → All Users, sort by newest, review the recent additions. Bulk-delete anything obviously bot-shaped (random gibberish usernames, throwaway email domains, no profile data). Then add a captcha to your registration form (Royal Comply or the GuardPress Turnstile integration, depending on your setup).
Safe to ignore
If you run a busy community/shop/membership and 10+ daily registrations is your normal cadence, this finding will fire constantly. You can leave it; the Critical/High classification doesn’t escalate to anything that bricks the site, just an email.
Low Users Without Email
One or more user accounts have no email address.
Check
SQL count of users with user_email = '' or user_email IS NULL.
Why it matters
WordPress’s standard registration flow always sets an email. Empty-email accounts came from somewhere else — a database import that lost the column, direct SQL inserts, or (rarely) an injected account from a poorly-formed exploit. Often benign; occasionally a tell.
Fix
Open the user in Users → All Users and either set a valid email or delete the account if it shouldn’t exist.
Low Orphaned Post Meta / User Meta / Term Relationships
Meta rows pointing at deleted posts, users, or terms accumulated past the threshold (100 postmeta / 50 usermeta / 100 term-relationship rows).
Check
Three independent LEFT JOIN counts: postmeta vs posts, usermeta vs users, term_relationships vs posts. Each fires when its threshold is exceeded.
Why it matters
Performance, not security. WordPress doesn’t always clean up child rows when a parent is deleted (especially older plugins / custom code), so an old, busy site accumulates orphans that bloat the database and slow down meta queries.
Fix
The Database Security page has a one-click Clean Up button for each of these (the finding has fix_available = true). The cleanup runs a DELETE ... LEFT JOIN on a 5-minute time limit / 256MB memory limit AJAX worker, which is safe on installs up to several hundred thousand orphans.
Safe to ignore
Yes, indefinitely. Orphaned meta is wasted space, not a security risk. If your install is performing fine and you don’t need to reclaim the bytes, skip the cleanup.
High / Medium Large Autoloaded Options
Total size of autoload = 'yes' rows in wp_options exceeds 1 MB (Medium) or 3 MB (High).
Check
SELECT SUM(LENGTH(option_value)) FROM wp_options WHERE autoload = 'yes'. Threshold-laddered.
Why it matters
WordPress loads every autoloaded option into memory on every single page request — including admin, REST, AJAX, and cron. A 3MB autoload payload adds a fixed cost to every request. The usual cause is a misbehaving plugin storing megabytes of cache / settings / logs as an autoloaded option instead of using transients or a custom table.
Fix
Identify the culprit. SSH into the database or use a query tool:
SELECT option_name, LENGTH(option_value) AS bytes
FROM wp_options
WHERE autoload = 'yes'
ORDER BY bytes DESC
LIMIT 20;The top of the list is almost always one or two plugin-prefixed options. Identify the plugin (search plugin directory for the prefix), then either disable that plugin’s data-bloat feature or set the option to autoload = 'no' if it doesn’t need to be loaded on every request:
UPDATE wp_options SET autoload = 'no' WHERE option_name = '...';Safe to ignore
Up to ~1MB is genuinely fine on modern hosting. Past 3MB it shows up as user-visible slowness. The High threshold corresponds to a real performance regression.
Low Expired Transients
More than 100 expired transient rows are sitting in wp_options.
Check
SQL count of _transient_timeout_* rows whose value (a UNIX timestamp) is less than time().
Why it matters
WordPress lazy-deletes expired transients (only on the next read), so a site that writes a lot of transients but reads them rarely accumulates dead rows. Like orphaned meta — performance / cleanliness, not security.
Fix
One-click Clean Up on the Database Security page. Deletes all expired transient timeout rows AND all transient value rows (the cleanup is aggressive — clears expired and unexpired alike; legitimate transients will be regenerated on next access).
Safe to ignore
Yes. If the count stays in the low thousands, leave it; WordPress will eventually clean them up via its lazy-delete path.
Low Post Revisions
More than 500 post-revision rows exist in wp_posts.
Check
SQL count of posts with post_type = 'revision'.
Why it matters
WordPress stores a revision every time you save / autosave a post. On a site that’s been live for years with frequently-updated posts, revisions can easily outnumber published posts 10-to-1, bloating the database and slowing down post queries that JOIN against wp_posts.
Fix
- One-click Clean Up on the Database Security page deletes ALL revisions in one query.
- Cap future accumulation by adding
define( 'WP_POST_REVISIONS', 5 );towp-config.php— WordPress will then keep only the 5 most recent revisions per post.
Safe to ignore
Yes, if you actively use revision history to recover prior versions. The cleanup is destructive — once deleted, the prior versions are gone.
Low Spam Comments / Trashed Comments
More than 100 spam comments or more than 50 trashed comments in wp_comments.
Check
Two SQL counts: comment_approved = 'spam' (threshold 100) and comment_approved = 'trash' (threshold 50).
Why it matters
Akismet and equivalents catch spam comments and put them in the spam queue but don’t auto-delete them. Over months/years a busy comment site accumulates tens of thousands of spam rows that never get reviewed.
Fix
One-click Clean Up on the Database Security page, or empty the spam / trash queues from WP Admin → Comments.
Safe to ignore
Yes. Spam comments don’t affect the site; they just take up space.
Low Trashed Posts
More than 50 posts in the trash.
Check
SQL count of posts with post_status = 'trash'.
Why it matters
Same cleanup motivation as trashed comments. WordPress auto-deletes trashed posts after 30 days by default, but if you ever set EMPTY_TRASH_DAYS to 0 (disabled) or some plugin changed the value, trash accumulates indefinitely.
Fix
One-click Clean Up, or empty trash manually from Posts → All Posts → Trash → Empty Trash.
High Outdated MySQL Version
Your MySQL (or MariaDB compatibility layer) version is older than 5.7.
Check
$wpdb->db_version() compared with version_compare against 5.7.
Why it matters
MySQL 5.6 and earlier no longer receive security updates from Oracle. Sites running them are accumulating unpatched database-server vulnerabilities at the engine level. Many newer WordPress features (and plugins) also assume 5.7+ behaviours (JSON columns, utf8mb4 indexes at full width, etc.) and silently degrade on older versions.
Fix
Upgrade via your hosting control panel. Most managed hosts offer MySQL/MariaDB version switching as a one-click option in cPanel / Plesk / WHM / the host’s dashboard. Take a backup first via SiteVault Pro; in-place engine upgrades are normally safe but data-layer regressions exist.
Safe to ignore
No. The recommendation in the finding text targets MySQL 8.0+, but anything 5.7+ removes the High alert.
Critical Suspicious Post Content / Suspicious Options Data
Posts or non-transient wp_options rows contain code patterns consistent with injected malware.
Check
SQL LIKE scan against a curated pattern list covering: eval(base64_decode(...)), eval(gzinflate(...)), eval(gzuncompress(...)), eval(str_rot13(...)), assert(base64_decode(...)), preg_replace with the deprecated /e modifier, raw <?php tags inside post content, hidden / zero-dimension iframes, document.cookie exfiltration patterns, cryptominer references (coinhive, crypto-loot, coinimp, webminepool), and known shell signatures (c99shell, r57shell, WSO shell). Transient options are excluded to avoid false positives from cached page HTML.
Why it matters
These shapes are the signature of compromised WordPress installs. Attackers inject PHP into post content (so it executes when a page loads via filter callbacks that eval() shortcode-like content) or into the options table (so plugins that maybe_unserialize + eval trigger payload). Patterns are filterable via guardpress_db_malware_patterns.
Fix
This is a probable active compromise:
- Take an immediate backup (do NOT overwrite a clean prior backup — you may need both)
- Open the affected rows. The finding tells you whether it’s posts (
edit.php) or options. For posts: review each, clean or delete. For options: identify the option name + the plugin that owns it; that plugin is likely the entry point - Run the full GuardPress malware scanner (GuardPress → Malware Scanner) on plugin / theme files to find the injection source
- Rotate ALL passwords, salts, API keys
Safe to ignore
If you legitimately publish PHP code in posts (developer / tutorial blog), the post-content scan may false-positive on tutorials about eval or base64_decode. Filter the pattern list via guardpress_db_malware_patterns from a must-use plugin to remove the patterns that match your content. Don’t globally suppress — remove only the patterns that fire on your specific content.
Critical active_plugins Option Corrupted / Contains Suspicious Entries
The active_plugins option in wp_options is not a normal serialized array of plugin paths.
Check
Two checks. (a) The option is read and verified to be an array; non-array = critical. (b) Each entry is verified to look like a plugin basename (matches ^([a-z0-9_.\-]+/)?[a-z0-9_.\-]+\.php$), is not empty, contains no base64_decode / eval( / <?php / .. / NUL bytes, and does NOT reference mu-plugins/ (mu-plugins load automatically; they should never appear in active_plugins).
Why it matters
Attackers who want to autoload injected code sometimes write malformed entries into active_plugins — bypassing the WP plugin loader’s normal validation. Any entry that doesn’t look like a real plugin path is either tampering or serious database corruption. Up to 5 example offending entries are included in the finding description.
Fix
Open WP Admin → Plugins. The deactivate/activate plugin list shows what WordPress thinks is active. Compare against the offending entries from the finding. Deactivate anything suspicious from the UI (which cleanly rewrites active_plugins). If the option is corrupted to the point that WP Admin won’t load, you may need to fix it directly via wp-cli: wp option get active_plugins --format=json, edit the JSON, wp option update active_plugins '...'.
Safe to ignore
Never. This is one of the highest-signal indicators of compromise the scanner produces.
Critical Poisoned Capability Metadata
Capability or user_level usermeta rows contain code-like patterns (eval / base64 /
Check
Same pattern list as the post / options content scan, applied to usermeta rows with meta_key matching {prefix}_capabilities or {prefix}_user_level. These rows should always be PHP-serialized arrays; code patterns indicate tampering.
Why it matters
Some role-escalation backdoors work by writing executable PHP into a capability row, then triggering deserialization of that row to eval() the payload.
Fix
Treat as active compromise; same response as “Suspicious Post Content” above.
High Recently Created Administrator Accounts
One or more accounts with the administrator capability were created in the last 30 days.
Check
SQL JOIN of wp_users + wp_usermeta for users with {prefix}_capabilities LIKE '%administrator%' and user_registered > NOW() - 30 days.
Why it matters
Stealth admin creation is a common post-compromise backdoor. Attackers create a low-profile admin account so that even after you clean up the obvious payload, they can log back in. A scheduled scan that surfaces brand-new admins for review catches this class.
Fix
Open Users → Administrators, sort by registration date, review each new admin. Anyone you didn’t personally invite should be deleted immediately, then rotate passwords + salts.
Safe to ignore
If your team genuinely onboarded a new admin recently (you know who, you invited them), confirm the account matches the expected user and move on.
High Admin Users Missing user_level Metadata
A user has the administrator capability but no corresponding user_level usermeta row.
Check
SQL LEFT JOIN between the capabilities row (containing administrator) and the user_level row; counts where the level row is missing.
Why it matters
When WordPress assigns a role through its normal API, it writes BOTH the capabilities row and the user_level row together. An admin-capable user with no user_level row suggests the capability was injected directly into usermeta (bypassing the role API) — a stealth-admin pattern.
Fix
Investigate the user. If legitimate, re-save their role from Users → Edit User to repair the missing row. If unexpected, delete the account and follow the compromise response above.
Critical Suspicious Scheduled Cron Events
Scheduled WP-Cron hook names contain malware patterns or look injection-shaped.
Check
Walks _get_cron_array() and checks each hook name for substrings base64_decode, eval(, <?php, gzinflate, str_rot13, shell_exec, OR matches against a 24+ character pure-hex regex (which catches randomly-named hooks injected by automated payloads). Up to 5 example offending hook names are included in the finding.
Why it matters
WP-Cron is a common foothold — an attacker schedules a recurring event that re-introduces their payload on every run, defeating partial cleanup attempts. Detecting code-patterned hook names catches the most common shapes.
Fix
List your scheduled events: wp cron event list. Confirm the suspicious hook names are present. Delete each: wp cron event delete <hook>. Re-scan to confirm. Then investigate WHY a rogue hook was scheduled — the scheduler was a legitimate WP API call from compromised code somewhere.
Safe to ignore
Never. Code-patterned cron hook names are essentially diagnostic for compromise.
Low Zombie Cron Events
5 or more scheduled cron hooks have no registered listener.
Check
For each scheduled hook, has_action($hook_name) returns false — nothing is listening for it. Only surfaced when the unique zombie count is 5 or higher (1–4 zombies is normal from a recently-removed plugin and gets suppressed).
Why it matters
Plugins that uninstall poorly leave their scheduled events behind. The events fire forever, find nothing listening, and burn a small amount of work on every WP-Cron tick. Mostly hygiene, occasionally a tell that a plugin was force-deleted without going through proper deactivation.
Fix
wp cron event list to inspect, wp cron event delete <hook> to remove each.
Safe to ignore
Yes. Zombies don’t cause failures; they cost ~zero CPU each.
One-Click Cleanup — Which Findings Support It
Findings with fix_available = true show a Clean Up button on the Database Security admin page. The button POSTs to the guardpress_optimize_database AJAX handler, which runs the deletion under a 5-minute time limit and 256MB memory budget. The handler covers:
- Orphaned post meta (
clean_orphaned_postmeta) - Orphaned user meta (
clean_orphaned_usermeta) - Orphaned term relationships (
clean_orphaned_terms) - Expired transients — also deletes all transient value rows (
clean_transients) - Post revisions (
clean_revisions) - Spam comments (
clean_spam_comments) - Trashed comments (
clean_trash_comments) - Trashed posts (
clean_trash_posts)
Every cleanup is logged to the audit log as action database_cleanup with the count of items removed.
Cleanups are SQL DELETE statements with no soft-delete or recovery path. Take a backup before any cleanup you’re not 100% confident about — especially clean_revisions (irreversible loss of revision history) and clean_transients (deletes all transient values, expired or not).
FAQ
How often does the scan run?
Once a day on WP-Cron via the guardpress_daily_scan hook. You can also run it on demand via the Run Scan Now button. The on-demand path is registered unconditionally, so the button works even when the daily scheduled scan is toggled off.
I’m getting a Critical “Suspicious Post Content” finding but I’m a developer blog and I publish code about base64_decode all the time
Filter the pattern list. Drop this into a must-use plugin:
add_filter( 'guardpress_db_malware_patterns', function( $patterns ) {
// Remove the patterns that false-positive on legitimate code tutorials
return array_values( array_filter( $patterns, function( $p ) {
return strpos( $p, 'base64_decode' ) === false;
} ) );
} );Pare narrowly — remove only the patterns that fire on your real content. Don’t return an empty array; you’d disable the entire scan family.
Why does GuardPress flag “Too Many Administrators” at 6 instead of some bigger number?
Heuristic. Most WordPress sites have 1–3 real admins and accumulated abandoned admin accounts. Six is the conservative cutoff where the typical site has a problem worth surfacing. Genuine multi-admin sites can ignore the finding — it’s Medium, doesn’t emit an alert email.
What’s the difference between this and the Malware Scanner?
Database Security scans rows in the database for malware patterns. The standalone Malware Scanner (GuardPress → Malware Scanner) scans plugin / theme / uploads FILES on disk. Compromises usually have artifacts in both places, so a Critical finding from one should prompt running the other.
Can I disable individual sub-checks?
Not via settings. The pattern lists (malware patterns in particular) are filterable via guardpress_db_malware_patterns, which lets you suppress specific patterns. The sub-check structure itself is fixed in code — if you have a use case for disabling a specific sub-check (e.g. orphaned-meta detection is irrelevant on a freshly-imported site), email priority support and we’ll consider adding a filter.
Still Stuck? Email Priority Support
If a finding doesn’t match anything in this reference, or you’re trying to decide whether to act on a Critical finding for your specific situation:
Email support@royalplugins.com with the diagnostic info below. Priority email support is included with your GuardPress Pro license — typical response time is within 24 hours.
Information to include in your email
- GuardPress version from WP Admin → Plugins (1.6.10 or higher for the full malware / tampering check family)
- WordPress version from WP Admin → Updates
- The exact finding name and severity from GuardPress → Database
- The finding description text as it appears in the report (so we can tell which sub-check it came from and which threshold was crossed)
- For Critical malware findings: whether you’ve already taken a backup, and whether the site has recently shown other compromise indicators (file integrity alerts, unexpected admin notices, weird outbound traffic, customer reports of redirects)
- What you’ve already tried — especially if a one-click cleanup finished but the finding persists
- Hosting setup — managed (SiteGround, Kinsta, WP Engine) vs shared (Bluehost, HostGator) vs VPS — matters for some MySQL-upgrade and shell-access recommendations
Database Security overlaps with several other GuardPress modules:
- Outdated Software Check — What Each Finding Means — sister reference for the other recurring scan
- File Integrity Alert Investigation — if Critical database findings led you to look at file-level integrity
- Quieting Security Alert Email Flood — if Critical/High findings are emailing you every day
- Database Security overview — the short feature description on the main GuardPress support page
- SiteVault Pro — for backups before running cleanup actions