WordPress redirect hacks that live in your database

Why your file scan came back clean and the redirect didn’t

A clean Wordfence report and a clean Sucuri report can coexist with an active redirect that’s sending your mobile traffic to a pharmacy site — because the payload was never in the files. It’s sitting in a single row of wp_options, serialized inside an option named ihaf_insert_body, invisible to every file-layer scanner that exists, and it fires only when it sees a Googlebot IP address or a mobile user arriving from a search result on their first visit. Your incognito tests show nothing because the conditional logic is engineered to show you the clean page. The only way to find it is a targeted SQL pattern audit against the database itself — and most agencies have never run one.

How the conditional logic defeats your testing

The five incognito tests that came back clean aren’t evidence the site is fine — they’re evidence the payload is working correctly. This class of malware is engineered to show a clean page to anyone who looks like an investigator, and to redirect everyone else. The asymmetry between what your client reports and what you can reproduce is the signature of conditional logic.

The most important thing to understand about modern Googlebot cloaking is that it doesn’t rely on user-agent string matching. Your curl -A "Googlebot/2.1" command fails to trigger the redirect because the payload isn’t checking the UA string — it’s checking whether your IP address falls within Google’s actual crawler network ranges. Sucuri’s January 2026 analysis found a script using low-level bitwise operations to verify IP membership in Google’s crawler subnets, with full IPv6 support included. You can send the exact Googlebot UA string from a residential ISP address all day; the payload will categorize you as a human visitor and serve the clean page. The only thing that triggers the redirect is a verified crawler IP, and you cannot fake that from your browser or from a VPS.

Beyond the Googlebot check, these payloads stack additional conditions that filter out site owners by design. They typically check login state first — a logged-in WordPress admin sees the clean page regardless of IP or device. After that comes referrer checking (the redirect fires only for visitors arriving from a Google search results page), device-type gating (mobile UA patterns like android|iphone|ipad), and first-visit cookie logic that suppresses the redirect on any session that has already seen it. Stack those conditions together and the realistic universe of affected visitors is Google crawlers, plus search-referred mobile users on their first visit who aren’t logged in — precisely the population that never reports back to you directly.

One documented variant adds a time-throttle on top of the device and referrer checks: it uses WordPress’s set_transient() API to flag each IP address after a redirect fires, suppressing further redirects from that IP for 24 hours, according to one secondary writeup of a mobile-redirect case. The first time you test from your phone you might catch it — but if you already tested yesterday, or the payload fired when your client first loaded the page and they’re testing again from the same connection, it will show clean.

This pattern has a name. The pharma hack — also documented as the “conditional hack” or “Google Viagra hack” — has been serving pharmaceutical spam to search engine crawlers while showing clean pages to human visitors for years. The underlying evasion techniques have become more precise, not more exotic. This is opportunistic SEO spam tooling that has absorbed IP-verification and transient-throttling because those features work, and they’ve been circulating in the wild since at least 2021. It’s commoditized cloaking that most testing workflows aren’t built to catch.

The most reliable diagnostic available to you right now doesn’t require special tooling: open Google Search Console, navigate to URL Inspection, paste in the affected page URL, and run a Live Test. Search Console shows you the actual rendered HTML Googlebot received on that request — from a verified Google IP, with a real crawler user-agent. If you see script tags or redirect logic in that rendered HTML that don’t appear when you load the page in your browser, you’ve confirmed the conditional redirect without needing to replicate a Googlebot IP yourself. That gap between what Googlebot sees and what you see is the finding.

The SQL audit: where to look and what to run

Every query below is a SELECT — read-only, safe to run against a live site. Nothing here mutates data; that comes in the next section, behind a backup warning. Open phpMyAdmin, click the SQL tab for your WordPress database, and paste these in sequence. The goal is a list of rows that don’t belong.

Start with wp_options

The first sweep catches script tags, GTM references, and base64 decode calls anywhere in wp_options. The Sucuri case put the payload in ihaf_insert_body specifically, but the same pattern turns up in any header/footer option — so cast the net wide:

SELECT option_name, option_value
FROM wp_options
WHERE option_value LIKE '%<script%'
   OR option_value LIKE '%googletagmanager.com%'
   OR option_value LIKE '%base64_decode%';

phpMyAdmin truncates long option_value cells in the results grid, so you may see the first 100 characters of a serialized blob and think it’s clean. Run this alongside it to surface rows that are suspiciously large — legitimate theme mod arrays rarely exceed 50–80 KB; anything in the megabyte range warrants a closer look:

SELECT option_name, LENGTH(option_value) AS size
FROM wp_options
ORDER BY size DESC
LIMIT 20;

Patchstack flags siteurl and home value discrepancies, plus script or base64 content inside widget_*, theme_mods_*, and custom_css option rows, as primary audit targets for redirect hacks. For siteurl and home specifically, the check is a comparison, not a pattern match: pull those two rows and verify the values match what’s in Settings → General and in wp-config.php. A silent override — where the stored value has a script tag appended after the URL — won’t show up in your browser bar but will fire on every page load.

SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home', 'ihaf_insert_body', 'ihaf_insert_head');

Move to wp_posts

Injected scripts in post_content are less common than wp_options injections for redirect payloads, but the surface is real. Published posts are worth checking first — draft and trashed content is less likely to fire on public requests:

SELECT ID, post_title, post_type
FROM wp_posts
WHERE post_content LIKE '%<script%'
  AND post_status = 'publish';

One gap worth naming: Gutenberg stores block attributes inside HTML comment markers directly in post_content, in the form <!-- wp:html --><script ...><!-- /wp:html -->. A LIKE '%<script%' query will catch that particular pattern, but deeply nested innerBlocks can carry attribute data that a flat string search misses. Flag any wp:html blocks you find for manual inspection of the full post in the editor.

Finish with wp_postmeta

Page builder data — Elementor’s _elementor_data, Divi’s _et_pb_page_layout, and similar keys — lands in wp_postmeta as serialized JSON or PHP objects, a surface file-layer scanners cannot reach. This query previews the first 200 characters of any suspicious value so you can triage without drowning in full serialized payloads:

SELECT post_id, meta_key, SUBSTRING(meta_value, 1, 200) AS preview
FROM wp_postmeta
WHERE meta_value LIKE '%<script%'
   OR meta_value LIKE '%eval(%'
   OR meta_value LIKE '%base64_decode%';

False positives are real here — some legitimate plugins store base64-encoded images or minified inline scripts in postmeta. Use the SUBSTRING() preview and the meta_key column to orient yourself: a hit on _elementor_data that previews a <script src="googletagmanager.com/gtm.js"> tag you didn’t add is a finding; a hit on an image-optimization key storing a base64 PNG probably isn’t. Context is the triage mechanism, not the query alone.

Cleaning it out without breaking the site

Before you write a single mutating statement, take a full database backup. Not a plugin backup, not a hosting snapshot you’d have to file a ticket to restore — a local dump you control: mysqldump -u user -p dbname > backup-pre-cleanup-YYYY-MM-DD.sql. If the cleanup goes sideways, that file is the only thing standing between you and a broken site. Everything below assumes that file exists.

Now, before you open that UPDATE statement: check whether the row is serialized. Look at the raw option_value or meta_value content. If it starts with a:, O:, or s:, you’re looking at a PHP serialized string. PHP serialized strings encode string length as s:N:"value" — that N is a byte count that must match the actual string content. Run a SQL REPLACE() that changes the content inside that blob without updating N, and unserialize() will fail, silently corrupting widget configurations, theme settings, or Elementor layout data in the process. The site won’t throw an error you can easily trace — it will just stop rendering certain elements correctly, and you’ll spend an hour debugging something that looks like a theme problem.

For any value that might live inside a serialized blob, use serialization-aware tooling instead of raw SQL. WP-CLI’s search-replace command handles this correctly: wp search-replace 'badstring' '' --dry-run --all-tables-with-prefix. Run the --dry-run flag first so you can see exactly what would change before it changes. Even purpose-built tools carry risk if outdated — CVE-2023-6933 in Better Search Replace allowed unauthenticated attackers to exploit PHP object injection via deserialized input, a sobering reminder that “serialization-aware” doesn’t mean “safe to run on an unpatched version.” Keep any tool you use fully updated before you point it at a production database.

For isolated, non-serialized injections the cleanup is more straightforward. If your SQL audit surfaced a payload in the ihaf_insert_body row and you didn’t install the Insert Headers and Footers plugin yourself, that row has no legitimate claim to existence. After your backup is confirmed: DELETE FROM wp_options WHERE option_name = 'ihaf_insert_body';. If the plugin is legitimately installed and in use elsewhere on the site, restore the row’s value to a known-good state from a clean staging environment rather than deleting it — a DELETE that removes a row a legitimate plugin expects will cause PHP notices or broken functionality on the next page load.

A managed-host restore from backup can feel faster than surgical cleanup, and sometimes it is — but only if you know which backup predates the compromise. Backups taken after the initial injection carry the payload forward, and without identifying the specific rows involved, you have no reliable way to determine which snapshot is actually clean. The SQL audit gives you that anchor: the row name, the content, and the approximate injection date if your host logs option updates.

Post-cleanup, rotate all admin and database credentials, update every plugin and theme, and re-run both your file-layer scanner and a database scan. Then open Search Console URL Inspection again and run a Live Test on the affected URL. If you see no foreign script tags in the rendered HTML and the page looks correct, you have confirmation the conditional redirect is gone, not just suppressed. Wordfence, Sucuri, MalCare, and Patchstack should stay installed; they own the file layer and nothing here replaces that. If you want the wp_options and wp_postmeta audit running on a schedule across client sites — including recursive Gutenberg block parsing — Content Guard Pro’s free tier on WordPress.org covers those same surfaces.

Next step: run your first database audit

You now have three concrete, read-only SQL queries you can paste into phpMyAdmin right now: one for wp_options script tags and byte-count anomalies, one for wp_posts published content, and one for wp_postmeta builder data. Start there. Open Search Console URL Inspection and run a Live Test on any page that’s been reported as redirecting — the gap between what Googlebot sees and what you see in your browser is the diagnostic confirmation you need.

If you find a row, make a local database backup before touching anything: mysqldump -u user -p dbname > backup-pre-cleanup-YYYY-MM-DD.sql. For non-serialized injections in rows like ihaf_insert_body, a DELETE is safe. For anything inside a serialized blob, use WP-CLI’s search-replace with the --dry-run flag first. After cleanup, rotate credentials, update everything, and re-run Search Console’s Live Test to confirm the conditional redirect is gone. File-layer scanners stay installed — they own the filesystem. The database audit is the complement, not the replacement.

Facebook
Twitter
LinkedIn

Get security tips in your inbox.

Popular Posts

WordPress redirect hacks that live in your database
WordPress Database Hardening: Complete Security Guide
WordPress Page Builder Malware: Database Attacks in Elementor, Divi, and ACF
How to remove malware from your WordPress database: Complete guide
WordPress SQL injection prevention: A database-first approach

Categories

Scroll to Top