WordPress Plugins
Free Tools
Pricing Blog Case Studies Switch to Royal Plugin Graveyard Support My Account Cart
Support / GuardPress / Database Security Checks Explained

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.

Where to find the scan

WP Admin → GuardPress → Database. The page shows the last scan time, severity counts (Critical / High / Medium / Low / Auto-Fixable), and the full list of findings. Use Run Scan Now to re-scan on demand, or wait for the next daily scheduled scan.

When the Scan Runs and Where Findings Show Up

Two ways the scan kicks off:

Findings are stored in the guardpress_database_results WordPress option and surface in three places:

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.

Severities are heuristics, not a CVE rating

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
  1. Create a new admin user with a non-default username (your name, a randomised handle, anything but admin / administrator / root / wp_admin)
  2. Log in as the new user
  3. Delete the old admin user 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
  1. One-click Clean Up on the Database Security page deletes ALL revisions in one query.
  2. Cap future accumulation by adding define( 'WP_POST_REVISIONS', 5 ); to wp-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:

  1. Take an immediate backup (do NOT overwrite a clean prior backup — you may need both)
  2. 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
  3. Run the full GuardPress malware scanner (GuardPress → Malware Scanner) on plugin / theme files to find the injection source
  4. 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:

Every cleanup is logged to the audit log as action database_cleanup with the count of items removed.

No undo

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
Related GuardPress topics

Database Security overlaps with several other GuardPress modules: