By the time someone calls us about a hacked WordPress site, it's rarely because they noticed the SQL injection itself — it's because Google flagged the site, or a customer reported a strange redirect, or hosting support suspended the account for "malicious activity." SQL injection on WordPress is quiet by design: the admin dashboard usually looks completely normal while the attacker's changes sit in wp_options or wp_postmeta, invisible unless you know exactly where to look.
Where the injection usually comes from
Core WordPress itself is rarely the vulnerable component — the $wpdb API is safe when used correctly. The exposure almost always comes from a plugin or theme that builds a raw SQL string with unsanitized $_GET/$_POST input instead of using $wpdb->prepare(), or from a custom REST API endpoint registered without a capability check on who can call it. We see this most often in older, infrequently-updated plugins — ones that were fine when written but never received a security patch for a class of vulnerability that wasn't well understood five years ago.
Once an attacker has a working injection point, the typical objective isn't destruction — it's persistence and content control: inserting an administrator-level user into wp_users, adding hidden posts or modifying wp_options to serve spam or cloaked redirect scripts, and often planting a small backdoor plugin so they can regain access even after the original hole is patched.
Why cleanup alone isn't enough: if you clean the database but don't identify and close the actual injection point, the same query runs again on the next scan and you're back to square one within days. Finding the vulnerable component comes before cleaning, not after.
Finding the vulnerable component
We start by enabling WP_DEBUG_LOG and watching for SQL errors on the next few requests, then cross-reference the plugin activation/update timeline against when the site's behavior first changed — Search Console's "last crawled" data on the earliest injected page is often a more reliable timestamp than anything in the WordPress admin. From there it's a matter of grepping plugin source for raw query construction: $wpdb->query() or $wpdb->get_results() fed directly from request superglobals rather than through prepare() is the pattern we're looking for.
Once identified, the plugin gets disabled immediately — not deleted yet, since you'll want the file intact if you ever need to report the vulnerability to the author or confirm the fix in a later version.
Cleaning the database without breaking the site
Database cleanup is the part most site owners get wrong, because it's tempting to delete broadly instead of surgically. The safer sequence:
- wp_users / wp_usermeta: sort by
user_registeredand flag accounts created outside your normal onboarding pattern; checkwp_capabilitiesfor administrator-level roles assigned to accounts that shouldn't have them - wp_options: verify
siteurl,home, andadmin_emailagainst what they should be — these three are the most common injection targets because changing them redirects the entire site or hijacks password resets - wp_postmeta / wp_usermeta: a targeted query for
meta_value LIKE '%base64_decode%'or%eval(%finds most obfuscated payloads without a full table scan by hand - wp-content/plugins/ and theme directories: check for files with modification dates that don't correspond to any update or deploy you made
Full backups before and after each step matter here — not as a formality, but because a bad delete on a live options table can take the site down faster than the original breach did.
Closing the loop
After the vulnerable component is removed or patched and the database is clean, every credential that touched the compromised install gets rotated — database user, WordPress admin accounts, hosting/FTP access, and any API keys stored in options tables. We then run the site through a week of active monitoring: file-integrity checks against a known-clean baseline, Search Console watched for any newly indexed pages, and a repeat pass over wp_users to confirm no new accounts appear.
Longer term, the hardening that actually prevents a repeat is unglamorous: keep core, plugins, and themes on auto-update where feasible, remove anything not in active use (every inactive plugin is still a live attack surface until it's deleted, not just deactivated), and put a WAF in front of the install to catch injection payloads before they reach $wpdb at all.
For developers maintaining custom code on the same install: the fix is almost always the same one-line discipline — every query built from user input goes through $wpdb->prepare(), no exceptions, no "just this once" for an admin-only screen. Admin-only isn't the same as authenticated-and-authorized, and that gap is where most of these vulnerabilities actually live.
WordPress site showing signs of compromise?
We'll identify the entry point, clean the database without breaking anything live, and harden the install against re-infection.
Get Emergency Recovery Help