WordPress Site Hacked? What to Do in the First Hour
Contain it, preserve evidence, find how they got in, then rebuild from clean sources and rotate every credential. The order matters, and skipping a step is how sites get reinfected.
Google shows Japanese, pharmacy or casino pages under your domain that you never wrote. Find the cloaked spam, remove it at the source, then get the junk URLs dropped from Google.
You search for your business and Google shows pages under your domain with titles in Japanese, or pages selling pills, replica watches, casino bonuses or loans. You click one and see your normal homepage, or a “page not found” error. Search Console may report thousands of new indexed pages, or flag Hacked: URL injection or Hacked: Content injection under Security issues. This is the Japanese keyword hack and its cousins: SEO spam injected into your site to borrow your domain’s reputation in Google.
The spam is usually invisible to you because it is cloaked: the site serves it to Googlebot and to visitors arriving from Google, and shows you, the owner, the real site. That is why owners often hear about it months late. This guide shows you how to see what Google sees, where the generator code lives, how to remove it, and how to get the junk URLs out of Google. For the general first-hour routine (containment, evidence, passwords), follow the hacked site guide (FG-05) first, then come back here.
An attacker gets write access, usually through a vulnerable plugin or stolen password, and installs a small page generator that answers requests for URLs that never existed on your site (for example /abc123.html, /?p=some-product-name or /store/cheap-pills-online) with spam content, often fetched live from the attacker’s server. A spam sitemap tells Google about thousands of these URLs at once. Links from other hacked sites push them up the rankings.
The generator then decides who sees what. Typical checks:
| Check | What the code looks at | Effect |
|---|---|---|
| User agent | The browser identity string, looking for Googlebot, bingbot and similar |
Crawlers get spam, browsers get your site or a 404 |
| Referrer | Whether the visitor clicked through from a search engine | Searchers get redirected to the spam shop, you typing the URL do not |
| IP address | Whether the request comes from Google’s published crawler ranges, sometimes confirmed by reverse DNS | Defeats owners who fake the Googlebot user agent |
| Logged-in cookie | Whether a WordPress login cookie is present | Administrators never see anything wrong |
So the site “looking fine” to you proves nothing.
curl runs on your own computer (built in on macOS, Linux and Windows 10 or later; in PowerShell type curl.exe so you get the real curl rather than an alias). wp, grep and find run in an SSH terminal on the server, from the site folder that contains wp-config.php. On some hosts the command is wp-cli instead of wp. Words in CAPITALS are placeholders explained under each command.In Google, search for site:example.com, then narrow it with words the spam uses, for example site:example.com viagra, site:example.com casino, or a Japanese term such as site:example.com 通販 (mail order) or site:example.com 激安 (very cheap). Replace example.com with your domain. Result counts on site: searches are rough estimates, so use them to find the URL pattern, not to count pages.
Write down the shape the spam URLs share (a folder you never created, a query string, random .html names). It drives your cleanup and your 410 rule later.
Attackers often verify themselves as an owner of your property so they can submit their own sitemaps. Open Settings, then Users and permissions. Every person listed there should be someone you know. For owners, open the three-dot menu next to the name to see how they verified. An unknown owner verified with an HTML file or meta tag means the attacker placed that token on your site.
Remove the unknown owner, then remove their verification token too, or they can re-verify: the googleSOMETHING.html file in the site root, the google-site-verification meta tag in your theme or SEO plugin, or the DNS TXT record. Keep your own token.
In Search Console, paste one spam URL into the inspection bar at the top, then click Test live URL. When it finishes, click View tested page and open the HTML tab. This is the page as Google’s crawler, from Google’s IP addresses, just received it. Spam here while your browser shows a normal page confirms cloaking. Save the HTML as evidence.
Repeat on your homepage: some variants inject hidden spam links into real pages instead.
From your computer, request the same spam URL twice, once as a normal browser and once claiming to be Googlebot:
curl -s -o normal.html -w "%{http_code}\n" "https://example.com/SPAM_PATH"
curl -s -o bot.html -w "%{http_code}\n" -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" "https://example.com/SPAM_PATH"
Replace example.com with your domain and SPAM_PATH with the path part of a spam URL from your site: search (everything after the domain). -s hides the progress bar, -o saves the page to the named file, -w "%{http_code}\n" prints the HTTP status code, and -A sets the user agent string. Then compare the two files in a text editor, or run wc -c normal.html bot.html to compare their sizes.
Bad output: the normal request returns 404 and a small file, while the bot request returns 200 and a much larger file full of product text. Identical results do not clear you, because better generators check the IP address, which curl cannot fake; trust the live test over curl.
Check all five places below; many infections use two or three at once.
On Apache and LiteSpeed servers, the generator is often wired in with a rewrite rule that sends every non-existent URL to a PHP file the attacker uploaded. List every .htaccess file on the site:
find . -name ".htaccess" -exec ls -l {} \;
Type it as shown. It searches this folder and everything below it for files named .htaccess and prints each one with its size and date. A normal WordPress site has one in the root, and perhaps a few small protective ones inside plugin or uploads folders. A clean WordPress root file contains the standard block between # BEGIN WordPress and # END WordPress, which sends unknown URLs to index.php, plus any rules your caching or security plugin added. Bad signs: a RewriteRule pointing at a PHP file with a random name, RewriteCond %{HTTP_USER_AGENT} lines mentioning google or bot, or a RewriteRule that matches \.html$ and passes the name to a script.
Generators often store their pages as files on disk, sometimes thousands of them, in a folder with a plausible name. Look for recently created folders and unusually large numbers of files:
find . -type d -ctime -DAYS -not -path "./wp-content/cache/*"
find . -type f -name "*.html" -not -path "./wp-content/cache/*" | wc -l
Replace DAYS with how many days back you want to look; pick a number that reaches before the spam first showed up in your Performance report. -type d limits the first search to folders, -ctime matches by the time the folder was last changed on disk (harder for an attacker to fake than the modified date), and -not -path skips your page cache, which legitimately holds many files. The second line counts static HTML files; a WordPress site normally has very few, so a count in the hundreds or thousands outside the cache is a strong lead. Open any unfamiliar folder and look at its contents.
Then run wp core verify-checksums --include-root. The --include-root flag also reports non-WordPress files in the site root, where generator scripts like to sit. Good output ends with Success: WordPress installation verifies against checksums. Expect your own wp-config.php and verification files in the warnings; anything else that “should not exist” needs inspection.
Some variants insert thousands of posts, usually as a custom post type or as published posts with odd authors. Count posts by type and status:
wp post list --post_type=any --post_status=any --format=count
wp db query "SELECT post_type, post_status, COUNT(*) AS n FROM wp_posts GROUP BY post_type, post_status ORDER BY n DESC;"
The first command prints one number: every post of every type and status. --post_type=any and --post_status=any remove the default filters, and --format=count prints only the total. The second command breaks the total down. Run wp db prefix first and replace wp_ in wp_posts if your prefix differs. Good output looks like your real content plus revisions, attachments and menu items. Bad output is a post type you do not recognize with thousands of rows, or a publish count far beyond what you wrote.
To look inside, search the content for spam terms with WP-CLI’s database search:
wp db search "SPAM_WORD" --stats
Replace SPAM_WORD with a word from the spam titles, for example a drug name. The search runs across all WordPress tables and is case-insensitive by default. --stats adds a summary of how many matches it found in each table and column. Matches in wp_posts.post_content point to spam posts; matches in wp_options point to stored generator settings or cached spam. No matches means the content is coming from files or from the attacker’s server.
Open https://example.com/robots.txt. Every Sitemap: line should point to your own SEO plugin’s sitemap; attackers add their own lines, sometimes via a physical robots.txt that overrides WordPress’s virtual one. Then look for stray sitemap files:
find . -maxdepth 3 -iname "*sitemap*" -not -path "./wp-content/plugins/*"
Type it as shown. -maxdepth 3 keeps the search near the top of the site, -iname matches names containing “sitemap” in any letter case, and the -not -path part skips plugin code, which legitimately contains sitemap classes. Most WordPress SEO plugins generate sitemaps on the fly, so on many sites this finds nothing. A sitemap.xml or a run of numbered sitemap files you did not create, especially ones listing URLs that match the spam pattern, is injected.
Link-injection variants hide in the active theme’s functions.php or in a fake plugin, printing spam links only when the visitor is a crawler. Search code for user agent checks:
grep -rlE "HTTP_USER_AGENT.{0,80}(googlebot|bingbot|google)" --include=*.php wp-content
Type it as shown. -r searches every subfolder, -l prints only file names, -E enables the pattern syntax, --include=*.php limits it to PHP files, and the pattern finds code that reads the user agent and names a search engine within 80 characters. SEO and caching plugins match legitimately; a short, obfuscated file in a theme or unknown plugin folder does not.
Once you know where the spam lives, remove it at the source, in this order:
wp post delete $(wp post list --post_type=POST_TYPE --format=ids) --force. Replace POST_TYPE with the type you found in the count query. --format=ids prints the IDs as a space-separated list for the outer command, and --force skips the trash and deletes permanently. Review the list first by running the inner command alone. Never run this against post or page without filtering further.Sitemap: lines, and in Search Console’s Sitemaps report, open each sitemap you did not submit and remove it. Then resubmit your real sitemap.404 or 410, not a 200 page that redirects or shows your homepage.Google drops a URL from its index once it recrawls the URL and gets a “gone” answer. Both 404 (Not Found) and 410 (Gone) work. A 410 says the removal is deliberate and permanent, and Google staff have said it can be processed a little faster, but the difference is small. What matters far more is that spam URLs do not return 200, do not redirect to your homepage (Google may treat that as a soft 404 and keep the URL longer), and are not blocked in robots.txt. A blocked URL cannot be recrawled, so Google never sees that it is gone and may keep it listed.
If the spam shares a pattern, you can answer it with 410 at the server. On Apache or LiteSpeed, add this above the WordPress block in the root .htaccess (a file you edit, not a command):
RewriteEngine On
RewriteRule ^SPAM_FOLDER/ - [G,L]
Replace SPAM_FOLDER with the folder name the spam URLs share, for example a made-up directory like shop-jp. The ^ anchors the match to the start of the path, the - means “do not rewrite”, G returns 410 Gone, and L stops processing further rules. On Nginx the equivalent goes in the server block: location ^~ /SPAM_FOLDER/ { return 410; }, then reload Nginx. Test with curl -s -o /dev/null -w "%{http_code}\n" https://example.com/SPAM_FOLDER/test from your computer: 410 is good, 200 or 301 means the rule is not matching. Only use a pattern that can never match a real page. Random-looking URLs with no shared pattern can simply be left to return WordPress’s normal 404.
Search Console’s Removals report (under Indexing) hides URLs from Google results quickly, often within a day. A temporary removal lasts about six months and does not delete anything from the index, so you still need the 404 or 410 underneath it. Use the Remove all URLs with this prefix option to hide a whole spam folder in one request. It suits spam ranking for your brand name, not thousands of random URLs with no common prefix.
For your own pages that were altered, use URL Inspection and Request indexing so the clean versions are crawled. Do not request indexing for spam URLs.
If Security issues lists a hacked category, request a review once you have confirmed the cleanup with live tests, and describe what you found and removed. Google states that reviews of hacked-with-spam sites can take several weeks because they may involve manual checks or reprocessing of the affected pages. The review request is covered in more depth in FG-24.
The honest recovery timeline:
site: searches for several months. That is normal if the live test shows 404 or 410.Your access log shows exactly what the server sent to Googlebot. On the server, filter Googlebot requests for spam paths:
grep -i "googlebot" ACCESS_LOG | grep "SPAM_PATTERN" | awk '{print $4, $9, $10, $7}' | head -50
Replace ACCESS_LOG with the path to your access log (your hosting panel’s logs section shows it; use zgrep for files ending in .gz) and SPAM_PATTERN with a piece of text all spam URLs share. grep -i matches Googlebot in any letter case. In the common log format, awk field 4 is the timestamp, 9 the status code, 10 the response size in bytes and 7 the requested path. Bad output: rows with status 200 and sizes of tens of kilobytes for paths that should not exist. After cleanup the same rows should show 404 or 410 with small sizes.
Anyone can claim to be Googlebot. To confirm a request is genuine, run host IP_ADDRESS (the first field of the log line): it should return a name ending in googlebot.com or google.com, and host on that name should return the same IP.
The earliest 200 on a spam path dates the infection and tells you which backups predate it.
Open a ticket if spam pages keep returning after cleanup, if you find generator files outside your site folder, or if you cannot read your access logs. Send:
.htaccess rules and file paths you found, with their creation times.If the spam keeps coming back after a full cleanup, work through the reinfection guide (FG-26).
Did this fix it?
Still stuck? Start again from what you can see in the Emergency Room.
Still stuck?
Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.
Get the free checklistsKeep reading
Search the field manual
Or jump straight to a symptom
Press / to search from any page, Esc to close