WordPress redirect hack: how to find and remove it

Visitors land on spam, often only on phones or when they come from Google. Nine steps to find where the redirect lives, take it out, and close the way in so it isn't back next week.

Founder of RoundBorders · WordPress since 2012

Published 11 min read

This post has you renaming folders and editing files on your server. Back up first and change one thing at a time, so you can undo the one that made it worse. If you'd prefer someone else did it, the end of the post says what we charge.

The short answer

A redirect hack lives in one of four places: .htaccess, the site address settings, a script injected into the database, or a modified PHP or JavaScript file. Test the site the way a visitor reaches it to see which kind you have, remove it there, then find what put it there, or it comes back.

  1. 01Copy the site as it is
  2. 02See the redirect the way a visitor does
  3. 03Check .htaccess, and anything that runs before WordPress
  4. 04Check the site address settings
  5. 05Search the database for injected scripts
  6. 06Compare every file with a clean copy
  7. 07Find what put it there
  8. 08Close the door
  9. 09Ask Google to lift the warning

Rather hand it over? Get a fixed price for the whole job, from $499.

A redirect hack sends your visitors somewhere you never chose: a pharmacy, a casino, a fake prize, a “your phone is infected” page. It rarely fires for everyone. The code checks who is asking and only sends phones, people arriving from a search result, first-time visitors or anyone who isn’t logged in, which is why the owner is usually the last to see it. I’ve cleaned hundreds of hacked WordPress sites, and taking the redirect out is the easy half. The other half is finding what put it there, because that is what brings it back.

If you aren’t sure yet that the site is hacked, how to tell if your WordPress site is hacked covers the signs and the first ten minutes. This post picks up where that one stops.

Step 01 Copy the site as it is

Before you change anything, take a full backup of the hacked site: every file, plus a database export. Label it, and keep it apart from your normal backups. It is your way back if a later step breaks something, and it keeps the evidence (the malicious code, the file dates) that shows how they got in.

With WP-CLI, export the database to your home folder, outside the public web root:

Terminal
wp db export ~/hacked-$(date +%F).sql

The files can come down through your host’s file manager as a zip, or over SFTP.

Before you move on Don’t restore an older backup yet. Until you know when the infection started, you can’t know whether that backup is clean, and restoring it wipes the trail you’re about to follow.

Step 02 See the redirect the way a visitor does

You need to make it happen on demand, because what you see decides where you look. The easiest test is your phone: a private tab, Wi-Fi off, search Google for your business and tap your own result.

From a computer, curl can pretend to be that phone. Replace yoursite.com with your address and run:

Terminal
UA="Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1"
curl -s -o /dev/null -D - -A "$UA" -e "https://www.google.com/" https://yoursite.com/

That prints the response headers only. Look at the first line and the location: header:

  • 301 or 302, with a location: on a domain that isn’t yours. The server sent the redirect before any page existed. It’s in .htaccess, in the site address settings, or in PHP.
  • 200. The page loaded, and something inside it moves the browser along. That is JavaScript, usually injected into the database or added to a script file.

For the 200 case, list the scripts the page loads and the usual hiding tricks:

Terminal
curl -s -A "$UA" -e "https://www.google.com/" https://yoursite.com/ | grep -oiE '<script[^>]+src="[^"]+"|eval\(|atob\(|fromCharCode|window\.location[^;]*'

Your analytics, chat widget and payment scripts will show up too. You’re looking for a domain you don’t recognise, or code that builds an address out of numbers.

Run the first command once more without -A "$UA" and -e …. A clean answer there and a redirect as a phone from Google confirms the code is choosing its victims. To see what Google gets, open Search Console, paste the address into URL Inspection, run Test live URL and open View tested page: hacks that show spam only to search engines are visible in that HTML.

Plain English A 301 or 302 is the server saying “go elsewhere” before your page is built. A 200 that still jumps is your own page doing it. That one difference halves where you need to look.

Some redirects fire once per visitor (they set a cookie), or only at certain hours, or only for some countries. If it happened once and won’t happen again, test from another network and ask whoever reported it for the exact page, device and time.

A 301 or 302?

Start at step 03. The server or PHP is sending it.

A 200 that still jumps?

Start at step 05. It's a script inside the page.

Step 03 Check .htaccess, and anything that runs before WordPress

On Apache and LiteSpeed servers, which is most shared hosting, the .htaccess file in the WordPress folder runs before WordPress does. That makes it a favourite place for a conditional redirect. Open it in your host’s file manager (turn on “show hidden files”). A clean WordPress block looks like this:

Apache
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

Caching and security plugins add blocks of their own, each between BEGIN and END comments that name the plugin. What doesn’t belong is a rule that checks where the visitor came from or what device they use, then sends them to someone else’s domain:

Apache
RewriteCond %{HTTP_REFERER} (google|bing|yahoo) [NC,OR]
RewriteCond %{HTTP_USER_AGENT} (android|iphone|mobile) [NC]
RewriteRule ^(.*)$ https://spam.example/ [R=302,L]

It may sit above # BEGIN WordPress, in the middle of a plugin’s block, or below hundreds of blank lines, so scroll to the very end. Then look for copies in subfolders, and for PHP settings that load a file before every page:

Terminal
find . -name .htaccess
grep -rn "auto_prepend_file\|auto_append_file" --include=.htaccess --include=.user.ini --include=php.ini .

auto_prepend_file tells PHP to run a file before every request, ahead of WordPress itself. Security firewalls use it on purpose (Wordfence points it at wordfence-waf.php). Anything else it points to deserves a close read.

Delete the rule, keep a copy of it in your notes for later, and save. If you can’t tell what a block is for, keep only the WordPress block above, then go to Settings → Permalinks and press Save so WordPress writes its own rules again; caching plugins put theirs back when you save their settings.

On Nginx Nginx ignores .htaccess. If your host runs Nginx and the redirect is a 301 or 302, it comes from PHP or from server settings only your host can see, so go on to step 04 and ask the host to check their side too.

Redirect gone?

Don't stop yet. Something wrote that rule. Skip to step 07 to find it, or the rule will be back.

Still redirecting?

Step 04.

Step 04 Check the site address settings

When every page redirects, wp-admin and the login page included, look at the two settings that tell WordPress its own address: WordPress Address (URL) and Site Address (URL) under Settings → General. If the redirect keeps you out of the admin, read them with WP-CLI:

Terminal
wp option get siteurl
wp option get home

Without WP-CLI, they are the siteurl and home rows of the wp_options table in phpMyAdmin (wp_ is the default prefix; yours is the $table_prefix line in wp-config.php). Check wp-config.php too: a define( 'WP_HOME', … ) or define( 'WP_SITEURL', … ) line overrides the database.

To put them back:

Terminal
wp option update home 'https://yoursite.com'
wp option update siteurl 'https://yoursite.com'

(If WordPress lives in a subfolder, siteurl ends with that folder.)

Redirect gone?

Skip to step 07. Someone with admin or database access changed it, and that access is still open.

Only some visitors, or only some pages?

Step 05.

Step 05 Search the database for injected scripts

A redirect that arrives as a 200 is usually a <script> tag injected into something that prints on every page: posts, widgets, theme settings, or a plugin’s “header and footer scripts” box. With WP-CLI:

Terminal
wp db search '<script' --all-tables-with-prefix --table_column_once
wp db search 'eval\(|atob\(|fromCharCode|document\.write' --regex --all-tables-with-prefix --stats

The first lists every table and column that holds a script tag. The second looks for the tricks injected code uses to hide what it loads (--regex is case-sensitive, which suits these). Without WP-CLI, run these in phpMyAdmin’s SQL tab, again with the default wp_ prefix:

SQL
SELECT ID, post_title, post_type, post_status FROM wp_posts WHERE post_content LIKE '%<script%';
SELECT option_name FROM wp_options WHERE option_value LIKE '%<script%';

An injection into posts tends to hit many of them at once, with the same line at the very start or the very end of the content. In the options you’ll also see your analytics or tag manager; the one to remove carries the domain you found in step 02.

To remove it from a handful of posts, open each one, switch to the Code editor (the ⋮ menu, top right) and delete the script. When hundreds of posts carry the identical line, wp search-replace can take it out in one pass. Copy the whole injected line exactly, and always run it with --dry-run first to see how many rows it would change:

Terminal
wp search-replace '<script src="https://spam.example/x.js"></script>' '' wp_posts --dry-run

Run it again without --dry-run when the count looks right.

Careful with options Most option values are stored serialized: the value carries its own length (s:42:"…"), and deleting characters in phpMyAdmin without fixing that number breaks the whole option. Remove the script from the screen that owns it (Appearance → Widgets, the theme’s settings, the plugin’s settings page). wp search-replace rewrites serialized values correctly, which is the other reason to use it.

Script gone and the page stays put?

Skip to step 07. Someone could write to your database. Find out how.

Nothing in the database?

Step 06. The code is in a file.

Step 06 Compare every file with a clean copy

Malware scanners look for code they already know. A checksum doesn’t care what the code looks like; it only asks whether a file is byte for byte the one WordPress.org published. That catches new tricks the scanners haven’t seen.

Terminal
wp core verify-checksums --include-root
wp plugin verify-checksums --all

For core, File doesn't verify against checksum means a WordPress file was changed, and File should not exist means an extra file sits among WordPress’s own, often with a name that looks like it belongs. --include-root checks the main folder too, leaving out the files that are supposed to be yours (wp-config.php, .htaccess, wp-content). For plugins, you get a table of files marked Checksum does not match or File was added. Premium plugins aren’t on WordPress.org, so the command skips them with a Could not retrieve the checksums warning, and there is no checksum command for themes at all.

Don’t edit an infected file to clean it. Replace it. For core, download the same version over the top, then delete each File should not exist file once you’ve looked at it (a download overwrites files but never removes extra ones):

Terminal
wp core download --version=$(wp core version) --skip-content --force

Without WP-CLI, WordPress’s own guide has you upload fresh wp-admin and wp-includes folders of the same version. Delete the old two folders first, so extra files go with them, and expect the site to be down for those minutes.

Reinstall each flagged plugin from WordPress.org (wp plugin install contact-form-7 --force, with each flagged plugin’s slug), and replace premium plugins and the theme with a fresh download from their vendor. A child theme that you or a developer edited has no clean copy anywhere, so read its PHP files, starting with functions.php, header.php and footer.php. These are the patterns worth a look across themes, plugins and must-use plugins:

Terminal
grep -rnE "eval\(|base64_decode\(|gzinflate\(|str_rot13\(|assert\(" wp-content/themes wp-content/plugins wp-content/mu-plugins --include=*.php

Plenty of honest plugins call base64_decode once. The line to worry about chains several of these together on a long string of random characters.

Clean copies in place and the redirect is gone?

Step 07 is what keeps it gone.

Everything verifies and it still redirects?

The code is somewhere these checks don't reach. Step 07 covers the WordPress places; anything past that is in the server's own settings.

Step 07 Find what put it there

The redirect is the part you can see. On a hacked site there is usually also something small whose only job is to let the attacker back in and put the redirect back: a file, an account, a scheduled task. If your redirect returns a day or a week after you removed it, this is why. Look in the places the WordPress admin doesn’t show you.

Must-use plugins and drop-ins. They load on every request and can’t be switched off from the Plugins screen.

Terminal
wp plugin list --status=must-use
wp plugin list --status=dropin

Managed hosts install must-use plugins of their own, and caching plugins install drop-ins (advanced-cache.php, object-cache.php). Anything you can’t match to your host or a plugin you use gets a close read.

Plugins you never installed. A plausible name, no page on WordPress.org, an author you’ve never heard of. wp plugin list shows them all, active or not.

Administrators you didn’t create. Note the dates before you delete anyone: an account made the week the trouble started puts a date on your timeline.

Terminal
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered

PHP files in uploads. The uploads folder holds images and documents. A one-line index.php from a plugin is normal; any PHP file with real code in it is not.

Terminal
find wp-content/uploads -name "*.php"

wp-config.php. Look for an include or require of a file you don’t recognise, and for a long encoded string near the top.

Scheduled tasks. A task you can’t match to a plugin, often running every few minutes, can be what writes the redirect back.

Terminal
wp cron event list --fields=hook,next_run_relative,recurrence

File dates help, but they can be faked. The access logs can’t, and they hold the way in: look for POST requests to plugin or theme files around the time the first odd file appeared. Step 5 of the first ten minutes says how to get them from your host.

Step 08 Close the door

  1. Update WordPress, every plugin and every theme. Delete inactive ones, and replace any plugin its developer stopped updating years ago.

  2. Change every password, from a clean device: each administrator, the hosting panel, SFTP, and the database user (then put the new database password in wp-config.php). Take the administrator role off anyone who doesn’t need it.

  3. Replace the secret keys in wp-config.php. That signs everyone out, including an attacker still holding a valid login cookie.

    Terminal
    wp config shuffle-salts

    Without WP-CLI, paste a fresh set from https://api.wordpress.org/secret-key/1.1/salt/ over the old lines.

  4. Turn off the file editor in the admin, so a stolen admin login can’t edit PHP from the browser. Add this to wp-config.php:

    PHP
    define( 'DISALLOW_FILE_EDIT', true );
  5. Clean every site on the same hosting account. One infected neighbour can reinfect the site you just cleaned.

When keeping everything updated is the part that slips, that is most of what a care plan does.

Step 09 Ask Google to lift the warning

If Google flagged the site (a warning under your search result, or Chrome’s red “Dangerous site” page), open Search Console and go to Security & Manual Actions → Security issues. A redirect usually appears as “Hacked: Code injection”, with sample URLs. The samples are not the full list, and Google is plain about it: fixing only some pages earns no partial return to search results.

Run the phone test from step 02 one more time. Then press Request review and describe what you found, what you removed and how you closed the way in. Google says malware reviews take a few days and spam reviews can take several weeks, and that the warnings come off within 72 hours of the site being found clean. Don’t send a second request while the first is open.

If the redirect came back after you removed it, if step 06 found changed files you can’t explain, or if there are no access logs to read, that is where I’d stop. The site still has a way in that you haven’t found, and each new round of deleting makes the trail harder to follow. Finding that door, not removing the redirect, is what the hacked site cleanup is for.

From the RoundBorders team

Redirect back after you removed it?

Send the address and what you noticed. We confirm it, send a fixed price, usually within a few hours, and clean it the same day.

  • From $499
  • Free re-clean for 30 days
  • Nothing to pay until you approve a price
No passwords needed to quote

Write as much as you like: what's happening, what you want, links, errors. The more we know, the more exact your fixed price.

How our hacked site cleanup works

Share this post

Know someone who thinks their site was hacked? Send it their way.

Bishoy Awad

Written by

Founder of RoundBorders. Building and fixing WordPress sites since 2012; our work is listed in the WooCommerce, Pressable and Jetpack partner directories.

Sources

Found a mistake? Write to me.

Keep reading

More on security