Skip to main content
Security Knowledge Base

Security Solutions
& Fix Guides

Step-by-step remediation guides for every vulnerability category our scanner detects. Find your issue below and follow the fix — no security expertise required.

HTTP / TLS

Your site is missing a valid SSL/TLS certificate, or the existing one is expired, self-signed, or misconfigured. Browsers warn visitors with a red "Not Secure" screen, and most will leave immediately.

How to Fix

Get a free certificate via Let's Encrypt

Most hosts offer one-click SSL. In cPanel go to SSL/TLS → Let's Encrypt. On the command line: sudo certbot --apache -d yourdomain.com

Force HTTPS in WordPress

Install Really Simple SSL plugin and click Activate SSL, or add to wp-config.php:
define('FORCE_SSL_ADMIN', true);

Verify & set auto-renew

Certificates expire every 90 days. Run sudo certbot renew --dry-run to confirm auto-renewal is set. Most hosts handle this automatically.

Verify the fix:

Visit your site with https:// and check for the padlock icon. Use SSL Labs for a deep certificate report.

Run a Free Scan Verify this issue on your site

Your server still accepts TLS 1.0 or 1.1, which have known cryptographic weaknesses. Attackers can force a downgrade and intercept traffic. Only TLS 1.2 and 1.3 should be accepted.

How to Fix

Disable old TLS in Apache

Edit your SSL virtual host config and set:
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
Then restart: sudo systemctl restart apache2

Disable old TLS in Nginx

In your nginx.conf or site config:
ssl_protocols TLSv1.2 TLSv1.3;
Reload: sudo nginx -s reload

Disable in cPanel/WHM

In WHM go to Service Configuration → Apache Configuration → Global Configuration and uncheck TLS 1.0 and 1.1 under SSL/TLS Protocols.

Verify the fix:

Run nmap --script ssl-enum-ciphers -p 443 yourdomain.com or use the SSL Labs test — look for "TLS 1.0" and "TLS 1.1" to show as not supported.

Run a Free Scan Verify this issue on your site

Users who visit http:// are not automatically redirected to the secure https:// version. They browse unencrypted without knowing it.

How to Fix

Apache .htaccess redirect

Add to your .htaccess:
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Nginx redirect block

Add a server block:
server { listen 80; server_name yourdomain.com; return 301 https://$host$request_uri; }

WordPress / Really Simple SSL

The Really Simple SSL plugin handles this automatically, including fixing mixed content URLs.

Verify the fix:

Run curl -I http://yourdomain.com and confirm you see 301 Moved Permanently with a Location: https:// header.

Run a Free Scan Verify this issue on your site

Your server supports weak or export-grade cipher suites (RC4, 3DES, NULL, EXPORT). These are cryptographically broken and allow attackers to decrypt traffic using BEAST, SWEET32, or similar attacks.

How to Fix

Use Mozilla's recommended cipher list (Apache)

In your SSL virtual host, set:
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder on

Nginx cipher hardening

In nginx.conf:
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

Use the Mozilla SSL Config Generator

Visit ssl-config.mozilla.org, select your server and profile (Modern/Intermediate), and copy the generated configuration directly.

Verify the fix:

Run an SSL Labs test and confirm Grade A or A+. Check that RC4, 3DES, and export-grade ciphers show as "No" in the cipher list.

Run a Free Scan Verify this issue on your site

The Expect-CT header is not present. Certificate Transparency logs are public records of all issued certificates that allow detection of misissued or rogue certificates for your domain. While modern browsers enforce CT automatically, the header can enforce stricter policies.

How to Fix

Add Expect-CT header (Apache)

Header always set Expect-CT "max-age=86400, enforce"
Note: Only add the enforce directive after verifying your certificate is in CT logs.

Add Expect-CT header (Nginx)

add_header Expect-CT "max-age=86400, enforce" always;

Monitor CT logs for your domain

Use crt.sh to search for all certificates ever issued for your domain. Set up alerts at CertSpotter to be notified of new certificate issuance.

Verify the fix:

curl -I https://yourdomain.com should include the expect-ct header. Verify your cert appears in crt.sh.

Run a Free Scan Verify this issue on your site

OCSP Stapling allows your server to pre-fetch and cache the certificate revocation status from the CA, then serve it directly to clients during TLS handshake. Without it, browsers make separate OCSP requests to the CA on every connection — slowing down page load and leaking visitor data to the CA.

How to Fix

Enable OCSP Stapling in Apache

Add to your SSL virtual host:
SSLUseStapling on
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

The cache directive goes in the global config, not inside a VirtualHost.

Enable OCSP Stapling in Nginx

Add to your server block:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;

Verify the fix:

Run openssl s_client -connect yourdomain.com:443 -status and look for OCSP Response Status: successful in the output.

Run a Free Scan Verify this issue on your site
Security Headers

HTTP Strict Transport Security (HSTS) tells browsers to always use HTTPS for your domain, even if a user types http://. Without it, network attackers can downgrade connections.

How to Fix

Add to Apache (.htaccess or vhost)

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

Add to Nginx

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

WordPress via plugin

Use HTTP Headers by WebFactory or Solid Security (formerly iThemes Security) to add HSTS without server access.

Verify the fix:

Run curl -I https://yourdomain.com and look for strict-transport-security in the response headers.

Run a Free Scan Verify this issue on your site

A Content Security Policy tells browsers which scripts, styles, and resources are allowed to load. Without it, your site is highly vulnerable to Cross-Site Scripting (XSS) attacks.

How to Fix

Start with a report-only policy

Add to your server config:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted-cdn.com;

Use a WordPress CSP plugin

HTTP Headers by WebFactory or WP Content Security Policy help you build and apply a policy without touching server config.

Tighten progressively

Once report-only shows no violations, switch to enforcement: Content-Security-Policy. Add nonces for inline scripts if needed.

Verify the fix:

Check the Network tab in browser DevTools — the response headers should include content-security-policy.

Run a Free Scan Verify this issue on your site

Without X-Frame-Options, attackers can silently embed your pages in invisible iframes on other sites, tricking users into clicking elements they can't see.

How to Fix

Apache

Header always set X-Frame-Options "SAMEORIGIN"

Nginx

add_header X-Frame-Options "SAMEORIGIN" always;

WordPress .htaccess

Add to the top of your .htaccess:
<IfModule mod_headers.c>
Header set X-Frame-Options "SAMEORIGIN"
</IfModule>

Verify the fix:

Check response headers via browser DevTools or curl -I https://yourdomain.com.

Run a Free Scan Verify this issue on your site

Without X-Content-Type-Options: nosniff, browsers may guess file types and execute malicious files as scripts or styles, even if served as plain text.

How to Fix

Add the header

Apache: Header always set X-Content-Type-Options "nosniff"
Nginx: add_header X-Content-Type-Options "nosniff" always;

Verify the fix:

Confirm x-content-type-options: nosniff appears in your response headers.

Run a Free Scan Verify this issue on your site

Without a Referrer-Policy, your full URL (including query parameters with sensitive data) may leak to external sites when users click links.

How to Fix

Add the header

Referrer-Policy: strict-origin-when-cross-origin
This is the recommended modern default — sends origin only on cross-site requests.

Verify the fix:

Confirm referrer-policy is present in response headers.

Run a Free Scan Verify this issue on your site

Without a Permissions-Policy header, scripts on your page can freely request access to the camera, microphone, geolocation, and other powerful browser APIs.

How to Fix

Add a restrictive policy

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Only allow features your site actually uses.

Verify the fix:

Check for permissions-policy in response headers.

Run a Free Scan Verify this issue on your site

Without a Cross-Origin-Opener-Policy header, cross-origin pages opened from your site may retain a reference to your window object, enabling cross-origin attacks such as Spectre-based side-channel exploits.

How to Fix

Add the header

Cross-Origin-Opener-Policy: same-origin
This severs the browsing context group from cross-origin openers. Use same-origin-allow-popups if your site uses OAuth or payment popups that need to communicate back.

Apache (.htaccess)

Header always set Cross-Origin-Opener-Policy "same-origin"

Nginx

In your server block:
add_header Cross-Origin-Opener-Policy "same-origin" always;

Verify the fix:

curl -I https://yourdomain.com should include cross-origin-opener-policy: same-origin.

Run a Free Scan Verify this issue on your site

Without a Cross-Origin-Resource-Policy header, your resources (images, scripts, fonts) can be loaded and read by any external website, enabling cross-site data leakage attacks.

How to Fix

Add the header

Cross-Origin-Resource-Policy: same-site
Use same-origin for stricter control, or cross-origin only for public CDN assets that are intentionally shared.

Apache (.htaccess)

Header always set Cross-Origin-Resource-Policy "same-site"

Nginx

add_header Cross-Origin-Resource-Policy "same-site" always;

Verify the fix:

curl -I https://yourdomain.com should include cross-origin-resource-policy: same-site.

Run a Free Scan Verify this issue on your site

Without Cross-Origin-Embedder-Policy, your site cannot enable powerful isolation features like SharedArrayBuffer and high-resolution timers needed to mitigate Spectre-class CPU side-channel attacks. When combined with COOP, COEP enables a fully cross-origin isolated browsing context.

How to Fix

Add the header (Apache)

Header always set Cross-Origin-Embedder-Policy "require-corp"
Note: This requires all subresources to opt-in via CORP or CORS headers. Test in report-only mode first: Cross-Origin-Embedder-Policy-Report-Only: require-corp

Add the header (Nginx)

add_header Cross-Origin-Embedder-Policy "require-corp" always;

Ensure all embedded resources opt in

Third-party resources (fonts, images, scripts) must serve either a Cross-Origin-Resource-Policy: cross-origin header or be served with CORS (Access-Control-Allow-Origin: *). Check browser DevTools console for COEP violations before enforcing.

Verify the fix:

curl -I https://yourdomain.com should include cross-origin-embedder-policy: require-corp. In Chrome DevTools, check for any COEP-related warnings in the Console.

Run a Free Scan Verify this issue on your site

The X-XSS-Protection header activates the built-in XSS filter in older browsers (IE, legacy Edge, older Safari). While modern browsers rely on CSP instead, setting this header is a quick win for legacy browser compatibility and defense-in-depth.

How to Fix

Add the header (Apache)

Header always set X-XSS-Protection "1; mode=block"
The mode=block directive tells the browser to block the entire page rather than sanitize and render it.

Add the header (Nginx)

add_header X-XSS-Protection "1; mode=block" always;

Prioritize a strong CSP

X-XSS-Protection is a legacy mechanism. Implement a full Content Security Policy (CSP) as the primary XSS defense — X-XSS-Protection complements it for older browsers that don't support CSP.

Verify the fix:

curl -I https://yourdomain.com should include x-xss-protection: 1; mode=block.

Run a Free Scan Verify this issue on your site

Sensitive pages (login, account, dashboard, checkout) lack Cache-Control: no-store headers. Browsers, proxy servers, and CDNs may cache authenticated page content, leaking private data to subsequent users on shared computers or through intermediate proxies.

How to Fix

Add no-store to authenticated pages (Apache)

Add to your VirtualHost or .htaccess for protected areas:
<LocationMatch "^/(wp-admin|account|dashboard|checkout)">
Header always set Cache-Control "no-store, no-cache, must-revalidate"
Header always set Pragma "no-cache"
</LocationMatch>

WordPress: add via wp_headers filter

Add to functions.php:
add_filter('wp_headers', function($headers) {
if (is_user_logged_in() || is_account_page() || is_checkout()) {
$headers['Cache-Control'] = 'no-store, no-cache, must-revalidate';
}
return $headers;
});

Configure your CDN/caching layer

In Cloudflare, use Page Rules to set Cache Level to "Bypass" for /wp-admin/*, /account/*, and /checkout/*. In WP Rocket or W3 Total Cache, exclude these pages from caching in their respective settings.

Verify the fix:

Log into your site, then run curl -I https://yourdomain.com/wp-admin/. The response should include cache-control: no-store.

Run a Free Scan Verify this issue on your site

External JavaScript and CSS files are loaded without Subresource Integrity (SRI) hashes. If a CDN you rely on is compromised, an attacker can replace the file with malicious code — and your visitors will execute it. SRI ensures browsers only execute files that match the exact expected hash.

How to Fix

Generate SRI hashes for your external resources

Use srihash.org to generate hashes. Then update your script/link tags:
<script src="https://cdn.example.com/lib.js"
integrity="sha384-abc123..."
crossorigin="anonymous"></script>

Add SRI to WordPress via filter

Add to functions.php:
add_filter('script_loader_tag', function($tag, $handle, $src) {
$sris = ['jquery-cdn' => 'sha384-your-hash-here'];
if (isset($sris[$handle])) {
$tag = str_replace('>', ' integrity="' . $sris[$handle] . '" crossorigin="anonymous">', $tag);
}
return $tag;
}, 10, 3);

Self-host critical dependencies

The safest approach is to self-host JavaScript libraries (jQuery, Bootstrap, etc.) rather than loading from external CDNs. This removes the third-party dependency risk entirely and often improves page load speed.

Verify the fix:

View page source and check that all external <script> and <link> tags have integrity="sha384-..." and crossorigin="anonymous" attributes.

Run a Free Scan Verify this issue on your site
WordPress

Attackers can detect WordPress from your HTML source and target known platform CVEs. Reducing fingerprinting limits automated attack surface.

How to Fix

Remove WordPress version from head

Add to functions.php:
remove_action('wp_head', 'wp_generator');

Rename or obfuscate wp-content paths

Plugins like WP Hide & Security Enhancer or WPS Hide Login change the default paths bots scan for.

Remove RSD and WLW links

Add to functions.php:
remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');

Verify the fix:

View your page source (Ctrl+U) and search for "wp-content" — ideally paths are obfuscated or removed.

Run a Free Scan Verify this issue on your site

Your exact WordPress version is visible in the HTML source. Attackers cross-reference this with CVE databases to instantly identify exploitable vulnerabilities.

How to Fix

Remove the generator meta tag

Add to functions.php:
remove_action('wp_head', 'wp_generator');

Remove version from scripts and styles

Add to functions.php:
add_filter('style_loader_src', function($src){ return remove_query_arg('ver', $src); });
add_filter('script_loader_src', function($src){ return remove_query_arg('ver', $src); });

Block /feed/ version disclosure

The RSS feed also exposes the version. Add: add_filter('the_generator', '__return_empty_string');

Verify the fix:

View source and search for your version number — it should not appear anywhere.

Run a Free Scan Verify this issue on your site

The readme.html file is publicly accessible and prominently discloses your WordPress version number.

How to Fix

Block access via .htaccess

Add to your .htaccess:
<Files readme.html>
Order Allow,Deny
Deny from all
</Files>

Or simply delete the file

Delete /readme.html from your WordPress root. It is not needed for site operation. Also delete readme.txt and license.txt.

Verify the fix:

Visit https://yourdomain.com/readme.html — you should get a 403 or 404.

Run a Free Scan Verify this issue on your site

Visiting /?author=1 redirects to a URL containing the admin username. Attackers use these usernames for brute-force and credential-stuffing attacks.

How to Fix

Block author scans via .htaccess

Add to .htaccess (before WordPress block):
RewriteCond %{QUERY_STRING} ^author=\d
RewriteRule ^ - [F]

Use a security plugin

Solid Security (iThemes) and Wordfence both have user enumeration blocking built-in under Brute Force Protection settings.

Disable the REST API user listing

Add to functions.php:
add_filter('rest_endpoints', function($endpoints){ unset($endpoints['/wp/v2/users']); unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']); return $endpoints; });

Verify the fix:

Visit https://yourdomain.com/?author=1 — the URL should NOT redirect to a path containing a username.

Run a Free Scan Verify this issue on your site

XML-RPC allows hundreds of login attempts per single HTTP request, making it a highly efficient brute-force vector. If you don't use it for Jetpack or mobile publishing, disable it.

How to Fix

Disable via .htaccess

<Files xmlrpc.php>
Order Deny,Allow
Deny from all
</Files>

Disable via functions.php

add_filter('xmlrpc_enabled', '__return_false');

Block but keep Jetpack

Use the Disable XML-RPC plugin which blocks external requests while still allowing Jetpack to function internally.

Verify the fix:

Visit https://yourdomain.com/xmlrpc.php — you should get a 403 Forbidden.

Run a Free Scan Verify this issue on your site

Your WordPress login is at the default /wp-login.php URL. Bots target this URL with automated brute-force attacks around the clock.

How to Fix

Rename the login URL

Install WPS Hide Login and set a custom slug like /my-secret-door under Settings → WPS Hide Login.

Add rate limiting or CAPTCHA

Wordfence or Solid Security limit login attempts and add CAPTCHA challenges. This is more robust than obscurity alone.

Restrict by IP if practical

In .htaccess:
<Files wp-login.php>
Order Deny,Allow
Deny from all
Allow from YOUR.OFFICE.IP
</Files>

Verify the fix:

Attempt to visit https://yourdomain.com/wp-login.php — it should return 404 or redirect.

Run a Free Scan Verify this issue on your site

The WordPress REST API at /wp-json/wp/v2/users publicly lists all usernames. This gives attackers half the information they need for a brute-force attack.

How to Fix

Disable the users endpoint

Add to functions.php:
add_filter('rest_endpoints', function($endpoints){ if (!is_user_logged_in()) { unset($endpoints['/wp/v2/users']); unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']); } return $endpoints; });

Use Solid Security or Wordfence

Both plugins include REST API user enumeration blocking in their settings under the API section.

Verify the fix:

Visit https://yourdomain.com/wp-json/wp/v2/users as a logged-out user — it should return empty or a 401/403.

Run a Free Scan Verify this issue on your site

The license.txt file in the WordPress root is publicly accessible. While low risk on its own, it confirms the site runs WordPress and can assist attackers in fingerprinting your exact core version.

How to Fix

Block access via .htaccess

Add to your .htaccess:
<Files license.txt>
Order Allow,Deny
Deny from all
</Files>

Or delete the file

Delete /license.txt from your WordPress root. It is not needed for site operation. You should also delete readme.html and readme.txt while you're there.

Verify the fix:

Visit https://yourdomain.com/license.txt — you should get a 403 or 404.

Run a Free Scan Verify this issue on your site

Your WordPress administrator account uses the username "admin" — the most targeted username in brute-force attacks. Attackers already know half your credentials; all they need to guess is the password.

How to Fix

Create a new administrator account with a unique username

Go to Users → Add New. Create a new account with a unique, non-obvious username and assign the Administrator role. Use a strong, random password.

Migrate content and delete the old admin account

Log out, then log in as the new administrator. Go to Users, hover over the "admin" user, and click Delete. When prompted, choose Attribute all content to your new account.

Change the display name if it reveals your username

Under Users → Profile, set Display name publicly as to your full name or pen name — never your login username.

Verify the fix:

Attempt to log in as "admin" — it should fail with "incorrect username". Run wp user get admin via WP-CLI — it should return an error.

Run a Free Scan Verify this issue on your site

WordPress pingbacks are enabled. Attackers abuse the pingback system to amplify DDoS attacks — sending thousands of forged pingback requests that cause your server (and other WordPress sites) to flood a victim with HTTP requests. This can also get your IP blacklisted.

How to Fix

Disable pingbacks globally

Go to Settings → Discussion and uncheck:
• "Allow link notifications from other blogs (pingbacks and trackbacks)"
• "Attempt to notify any blogs linked to from the article"
Click Save Changes.

Disable via functions.php

Add to functions.php:
add_filter('xmlrpc_methods', function($methods) {
unset($methods['pingback.ping']);
unset($methods['pingback.extensions.getPingbacks']);
return $methods;
});

Block the X-Pingback header

Add to functions.php:
add_filter('wp_headers', function($headers) {
unset($headers['X-Pingback']);
return $headers;
});

Verify the fix:

View page source and search for "pingback" — the X-Pingback link should not appear in the HTML head.

Run a Free Scan Verify this issue on your site

Anyone on the internet can register an account on your WordPress site. Unless you are running a membership site, this is unnecessary and opens your site to spam registrations, credential stuffing, and potential privilege escalation vulnerabilities in plugins.

How to Fix

Disable public registration

Go to Settings → General and uncheck Anyone can register. Set the New User Default Role to Subscriber even if you do need registration.

If registration is required, add CAPTCHA

Install WPForms or Simple Cloudflare Turnstile to add CAPTCHA to your registration form. This blocks automated bot registrations.

Add email verification for new accounts

Use WP-Members or User Registration plugin to require email confirmation before accounts become active. This prevents fake/spam registrations.

Verify the fix:

Visit https://yourdomain.com/wp-login.php?action=register — it should show "User registration is currently not allowed" if you have disabled it.

Run a Free Scan Verify this issue on your site

Your site emits a <link rel="wlwmanifest"> tag pointing to Windows Live Writer manifest file. This is a legacy feature from 2006 that serves no modern purpose, confirms the site runs WordPress, and adds an unnecessary HTTP request.

How to Fix

Remove the link tag

Add to functions.php:
remove_action('wp_head', 'wlwmanifest_link');

Remove RSD link at the same time

Add alongside:
remove_action('wp_head', 'rsd_link');
Both are legacy blogging client discovery endpoints with no value on modern WordPress sites.

Verify the fix:

View page source and confirm no wlwmanifest or rsd_link links appear in the <head> section.

Run a Free Scan Verify this issue on your site

The WordPress Heartbeat API sends AJAX requests to wp-admin/admin-ajax.php every 15–60 seconds from every logged-in user. On high-traffic admin sessions or if abused, this creates unnecessary server load. Attackers can also target admin-ajax.php with DoS requests.

How to Fix

Limit heartbeat to post editor only

Add to functions.php:
add_filter('heartbeat_settings', function($settings) {
$settings['interval'] = 60;
return $settings;
});

add_action('init', function() {
if (!is_admin()) wp_deregister_script('heartbeat');
});

Use the Heartbeat Control plugin

Install Heartbeat Control by WP Rocket (free) for a GUI to configure per-page heartbeat frequency or disable it entirely without code.

Block admin-ajax.php abuse at the WAF

In Cloudflare, create a rate limiting rule targeting POST requests to /wp-admin/admin-ajax.php from non-logged-in IPs. Set a threshold of 10 requests/minute.

Verify the fix:

Open browser DevTools → Network and filter for "admin-ajax". Confirm requests are infrequent (60-second intervals) or absent on non-editor pages.

Run a Free Scan Verify this issue on your site
WordPress Config

WP_DEBUG is enabled and PHP errors or WordPress notices are being output to the page. This exposes internal file paths, database query details, and code structure to anyone who visits your site — valuable reconnaissance data for attackers.

How to Fix

Disable debug in wp-config.php

Open wp-config.php and set:
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', 0);

Never display errors on production

Even if you need error logging, never display errors to users. Use WP_DEBUG_LOG to write to a log file, and set WP_DEBUG_DISPLAY to false.

Use a staging site for debugging

Enable WP_DEBUG only on staging/development environments. Most hosting control panels (cPanel, RunCloud, Kinsta) let you create staging clones in one click.

Verify the fix:

Visit your live site and check: no PHP warnings, notices, or errors should appear. Use curl https://yourdomain.com | grep -i "notice:\|warning:\|fatal".

Run a Free Scan Verify this issue on your site

Your WordPress installation is outdated. Outdated WordPress versions often contain publicly disclosed security vulnerabilities (CVEs). Attackers actively scan for and exploit known vulnerabilities in outdated WordPress cores. This is the single highest-impact thing you can fix.

How to Fix

Update via WordPress Dashboard

Go to Dashboard → Updates and click "Update to [latest version]". WordPress will download and install the update automatically. Always back up first.

Enable automatic core updates

In wp-config.php, add:
define('WP_AUTO_UPDATE_CORE', true);
This enables automatic security and minor release updates.

Update from WP-CLI

Via SSH: wp core update && wp core update-db

Back up before updating

Before any update, create a full backup with UpdraftPlus or your host's backup tool. This gives you a rollback point if anything breaks.

Verify the fix:

Check Dashboard → About WordPress — version should match latest at WordPress releases.

Run a Free Scan Verify this issue on your site

wp-cron.php is accessible via HTTP. WordPress's built-in cron system triggers via HTTP requests from visitors, which means it runs on every page load. On high-traffic sites this can create excessive load, and the public endpoint can be abused to trigger resource-intensive jobs repeatedly in a DoS-style attack.

How to Fix

Disable WP-Cron in wp-config.php

Add this to wp-config.php:
define('DISABLE_WP_CRON', true);

Set up a real server cron job

Add a cron job to your server:
*/5 * * * * curl -s https://yourdomain.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1
Or via WP-CLI:
*/5 * * * * wp cron event run --due-now

Block wp-cron.php from external access (optional)

Once you use a real server cron, optionally block the HTTP endpoint in .htaccess:
<Files "wp-cron.php">
  Order allow,deny
  Deny from all
</Files>

Verify the fix:

After setting DISABLE_WP_CRON, confirm cron jobs still run by checking scheduled events with wp cron event list.

Run a Free Scan Verify this issue on your site

The WordPress REST API is fully accessible without authentication. While some public endpoints are expected (needed for Gutenberg), leaving the full API open increases your attack surface. The /users endpoint in particular allows username enumeration.

How to Fix

Restrict unauthenticated REST API access

Add to your theme's functions.php or a custom plugin:
add_filter('rest_authentication_errors', function($result) {
  if (!is_user_logged_in()) {
    return new WP_Error('rest_not_logged_in', 'Authentication required.', ['status' => 401]);
  }
  return $result;
});

Disable the /users endpoint specifically

To only block user enumeration:
add_filter('rest_endpoints', function($endpoints) {
  if (isset($endpoints['/wp/v2/users'])) unset($endpoints['/wp/v2/users']);
  return $endpoints;
});

Use a plugin for granular control

Disable REST API or Solid Security offer settings to restrict REST API access without writing code.

Verify the fix:

curl https://yourdomain.com/wp-json/wp/v2/users should return a 401 or 403 after restriction.

Run a Free Scan Verify this issue on your site

No recognizable WordPress security plugin was detected on your site. Security plugins add critical protections: a Web Application Firewall (WAF), malware scanning, brute-force login protection, file integrity monitoring, and security audit logs. Running WordPress without one leaves you without a safety net.

How to Fix

Install Wordfence Security (free)

Go to Plugins → Add New and search "Wordfence". Install and activate. Run the Setup Wizard to configure the firewall, malware scanner, and brute-force protection.

Alternative: Solid Security (formerly iThemes)

Solid Security is another excellent free option with 2FA, brute-force protection, and security hardening. Available from the WordPress plugin repository.

Alternative: Sucuri Security (free + premium)

Sucuri offers both a free WordPress plugin (auditing, blacklist monitoring) and a premium cloud WAF that filters traffic before it reaches your server.

Verify the fix:

After installation, check the security plugin's dashboard for its status. Ensure the firewall is active and a scan shows no threats.

Run a Free Scan Verify this issue on your site

The built-in WordPress theme and plugin editors are enabled. If an attacker gains admin access, they can directly edit PHP files from the dashboard to inject backdoors — without needing FTP or server access. There is no reason to leave this enabled on a production site.

How to Fix

Disable via wp-config.php

Add to wp-config.php:
define('DISALLOW_FILE_EDIT', true);
This removes the Appearance → Theme Editor and Plugins → Plugin Editor menu items entirely.

Also disable file modification (optional but recommended)

To also prevent plugin and theme installation/deletion via the dashboard:
define('DISALLOW_FILE_MODS', true);
Note: This also blocks automatic core updates, so manage updates via WP-CLI or staging instead.

Verify the fix:

Log in as admin and check Appearance — the "Theme File Editor" option should be absent from the menu.

Run a Free Scan Verify this issue on your site

One or more WordPress plugins are outdated. Outdated plugins are the leading cause of WordPress compromises — many updates are released specifically to patch actively exploited security vulnerabilities. Plugin vulnerabilities are listed in public CVE databases and exploited en masse by automated bots.

How to Fix

Update all plugins via dashboard

Go to Plugins → Installed Plugins. Click Update Available to filter, then select all and use Bulk Actions → Update. Always backup first.

Enable automatic plugin updates

For each plugin, click Enable auto-updates in the plugin list. Or add to functions.php:
add_filter('auto_update_plugin', '__return_true');

Update via WP-CLI

Via SSH: wp plugin update --all
Check for updates: wp plugin status

Remove abandoned plugins

If a plugin hasn't been updated in over 2 years, check the WordPress.org page for its "Last Updated" and "Tested up to" fields. Abandoned plugins with no maintained security patches should be replaced with maintained alternatives.

Verify the fix:

Go to Dashboard → Updates — it should show "Everything is up to date!"

Run a Free Scan Verify this issue on your site

One or more installed WordPress themes are outdated. Even inactive themes can be exploited if they contain vulnerabilities — attackers can activate them via arbitrary file inclusion bugs. Themes should be kept updated or removed if unused.

How to Fix

Update active theme

Go to Appearance → Themes, hover over your active theme and click Update if available. Back up your child theme customizations first.

Delete unused themes

Keep only your active theme and one default WordPress theme (for emergency recovery). Delete all others via Appearance → Themes → Theme Details → Delete.

Use child themes for customizations

Never directly modify a parent theme — create a child theme instead. This allows parent theme updates without overwriting your customizations.

Verify the fix:

Dashboard → Updates should show no theme updates pending.

Run a Free Scan Verify this issue on your site

Your WordPress database uses the default wp_ table prefix. SQL injection attacks often assume this prefix — changing it makes automated SQL injection exploits targeting the default prefix fail even if a vulnerability exists.

How to Fix

Change prefix on new installations

In wp-config.php before installing, change:
$table_prefix = 'wp_';
to something like:
$table_prefix = 'sp7x_'; (use a random alphanumeric string).

Change prefix on existing sites (with caution)

Use Solid Security (iThemes) → Tools → Change Database Table Prefix. Always take a full database backup before this operation.

Manual prefix change via WP-CLI

wp search-replace wp_ newprefix_ --include-cols=option_name,user_meta --precise --no-report
Then rename the actual tables in MySQL. This is complex — use a plugin instead unless you're experienced.

Verify the fix:

Check wp-config.php$table_prefix should not be wp_. Confirm the site still loads correctly after the change.

Run a Free Scan Verify this issue on your site

WordPress secret keys and salts in wp-config.php are blank, set to placeholder values ("put your unique phrase here"), or using defaults. These keys are used to cryptographically sign authentication cookies. Without proper random keys, cookie forgery attacks become feasible.

How to Fix

Generate new keys from WordPress API

Visit api.wordpress.org/secret-key/1.1/salt/ to generate fresh random keys. This page generates new unique values every time you load it.

Replace the keys in wp-config.php

Open wp-config.php and find the eight define('AUTH_KEY'... lines. Replace the entire block with the newly generated values. Logged-in users will be logged out automatically — this is expected.

Rotate keys after any suspected breach

Rotating salt keys invalidates all existing sessions immediately. This is a useful emergency response when you suspect a session has been compromised.

Verify the fix:

Open wp-config.php and confirm all 8 keys/salts are set to long unique random strings — not "put your unique phrase here".

Run a Free Scan Verify this issue on your site

Your WordPress login form has no CAPTCHA or bot challenge. Automated scripts can attempt thousands of username/password combinations per minute without any friction. Even with rate limiting, a CAPTCHA adds a layer that stops bots entirely.

How to Fix

Add Cloudflare Turnstile CAPTCHA

Install Simple Cloudflare Turnstile plugin and enable it on the login page. Turnstile is free and privacy-friendly — it requires no user interaction for legitimate users (passes automatically).

Use Wordfence CAPTCHA

Wordfence includes a reCAPTCHA option for the login form under Wordfence → Login Security. Uses Google reCAPTCHA v3 (invisible).

Enable login attempt rate limiting

In addition to CAPTCHA, configure Wordfence or Solid Security to lock out IPs after 3–5 failed login attempts. Set lockout duration to 30+ minutes.

Verify the fix:

Visit your login page (or custom login URL) and confirm a CAPTCHA or bot challenge is present.

Run a Free Scan Verify this issue on your site

No activity logging plugin is installed. Without an audit log, you have no way to know who logged in, what changes were made, or when a compromise occurred. Activity logs are essential for detecting unauthorized access and post-breach investigation.

How to Fix

Install WP Activity Log plugin

Install WP Activity Log (free) — the most comprehensive WordPress audit trail plugin. It logs admin logins, content changes, plugin/theme changes, settings updates, and user activity.

Configure log retention and alerts

In WP Activity Log settings, set a reasonable log retention period (90+ days). Enable email alerts for critical events like new admin account creation, plugin installation, and failed logins.

Send logs off-site

Logs stored in WordPress's own database can be tampered with by an attacker who gains access. Use WP Activity Log's integration with external logging services (Papertrail, Loggly, Slack) to preserve logs off-site.

Verify the fix:

Log into WordPress and perform a change (e.g., edit a post). Check the audit log to confirm the action was recorded with timestamp and user info.

Run a Free Scan Verify this issue on your site

The wp-content/uploads/ directory allows PHP files to be executed. Since this directory is world-writable by the web server (for file uploads), an attacker who can upload a PHP file — via a vulnerable plugin, unrestricted file upload, or direct server access — can immediately execute arbitrary server-side code.

How to Fix

Block PHP execution in uploads (Apache)

Create a .htaccess file inside /wp-content/uploads/ with:
<Files *.php>
Order Deny,Allow
Deny from all
</Files>

Block PHP execution in uploads (Nginx)

Add to your server block:
location ~* /wp-content/uploads/.*\.php$ {
deny all;
return 403;
}

Also block other executable types

Extend the block to cover: .php, .php5, .phtml, .phar, .cgi, .pl, .py. Malicious uploads often use alternate extensions.

Verify the fix:

Upload a test PHP file to media library, then try to access it directly via URL — it should return 403 Forbidden.

Run a Free Scan Verify this issue on your site

No Web Application Firewall is protecting your site. A WAF inspects incoming HTTP requests and blocks malicious traffic — SQL injection, XSS attempts, file inclusion, and vulnerability scanners — before they reach your WordPress code. Without one, every attack attempt reaches your server directly.

How to Fix

Use Cloudflare (free plan includes WAF basics)

Put your site behind Cloudflare. The free plan includes DDoS protection, bot fighting, and basic security rules. The paid plans add the full OWASP rule set and custom firewall rules.

Install Wordfence (plugin-level WAF)

Wordfence includes an application-level WAF that runs within WordPress, blocking SQL injection, XSS, malicious file upload, and known exploit patterns before they reach your database.

Use Sucuri CloudProxy (premium cloud WAF)

Sucuri's WAF proxies all traffic through their infrastructure, filtering attacks before they reach your server. Includes virtual patching for known WordPress vulnerabilities.

Verify the fix:

Test your WAF using WAF testing tools or try a harmless XSS test string like ?q=<script>alert(1)</script> in a URL — a WAF should block or sanitize it.

Run a Free Scan Verify this issue on your site

No automated backup plugin or system was detected. Without regular backups, a malware infection, server failure, or accidental deletion results in permanent data loss. Backups are your last line of defense and recovery lifeline.

How to Fix

Install UpdraftPlus (free)

Install UpdraftPlus — the most popular WordPress backup plugin. Configure daily database backups and weekly file backups. Store backups remotely to Google Drive, Dropbox, or Amazon S3 — never just on your server.

Configure backup retention

Set UpdraftPlus to keep at least 7 daily and 4 weekly backups. In the event of a slow-burn compromise, you need to be able to restore a clean copy from before the infection began.

Test your backups

A backup you've never tested is not a backup. Quarterly, perform a test restore to a staging site to confirm backups are complete and restorable. UpdraftPlus has a built-in one-click restore.

Verify the fix:

Go to Settings → UpdraftPlus Backups and confirm the last backup date is within your scheduled interval. Check that remote storage is configured.

Run a Free Scan Verify this issue on your site

If your WordPress site uses WPGraphQL or a similar GraphQL endpoint, introspection is likely enabled. GraphQL introspection allows any user to query the complete schema of your API — discovering every type, query, mutation, and field name — giving attackers a detailed map of your data model and potential attack vectors.

How to Fix

Disable introspection in WPGraphQL

Add to functions.php:
add_filter('graphql_request_data', function($request_data) {
if (!is_user_logged_in() && isset($request_data['query']) && strpos($request_data['query'], '__schema') !== false) {
return ['query' => '{ __typename }'];
}
return $request_data;
});

Use GraphQL Armor or equivalent

Install a GraphQL security layer that disables introspection in production while allowing it in development environments based on authentication status or environment variable.

Restrict the GraphQL endpoint entirely if unused

If you installed WPGraphQL for a plugin dependency but don't actively use it, restrict the /graphql endpoint to authenticated users or internal IP ranges only.

Verify the fix:

Run: curl -X POST https://yourdomain.com/graphql -H "Content-Type: application/json" -d '{"query":"{ __schema { types { name } } }"}'
An unauthenticated introspection query should return an error, not your full schema.

Run a Free Scan Verify this issue on your site

No rate limiting was detected on your login page or contact forms. Without rate limiting, bots can make thousands of requests per minute — brute-forcing passwords, submitting spam, or scraping content. Rate limiting caps the number of requests from a single IP, making automated attacks impractical.

How to Fix

Enable Cloudflare Rate Limiting

In Cloudflare Dashboard → Security → WAF → Rate Limiting Rules, create a rule:
URL: /wp-login.php, Threshold: 5 requests / 1 minute, Action: Block for 1 hour.

Use Wordfence brute-force protection

In Wordfence → Firewall → Brute Force Protection, set "Lock out after X login failures" to 5 attempts, with a 30-minute lockout. Enable "Immediately block IPs that use an invalid username".

Rate limit via Nginx (server-level)

Add to your Nginx config:
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location = /wp-login.php {
limit_req zone=login burst=3 nodelay;
}

Verify the fix:

Attempt 10 rapid login requests to your login page — you should receive a block response (403) or be challenged before the 10th attempt.

Run a Free Scan Verify this issue on your site

WordPress files or directories have incorrect permissions — commonly 777 (world-writable) on directories or 666 (world-writable) on files. This allows any user on a shared server to read, modify, or delete your files, including configuration files with database credentials.

How to Fix

Set correct permissions via SSH

The recommended permissions for WordPress are:
# Directories
find /var/www/html -type d -exec chmod 755 {} \;
# Files
find /var/www/html -type f -exec chmod 644 {} \;
# wp-config.php
chmod 440 /var/www/html/wp-config.php

Set wp-config.php to 400 or 440

chmod 400 wp-config.php (only owner can read) or chmod 440 (owner and group can read). This prevents other server users from reading your database credentials.

Never use 777 permissions

777 means all users on the server can read, write, and execute the file. On shared hosting, this means all other sites on the server can access your files. Only uploads directories occasionally need 755 for the web server to write to them.

Verify the fix:

Run stat wp-config.php and confirm permissions show -r-------- (400) or -r--r----- (440). Run find . -type d -perm 777 to find world-writable directories.

Run a Free Scan Verify this issue on your site

PHP files in the wp-includes/ directory can be accessed directly via HTTP. While most are harmless, some can be used as attack vectors (e.g., wp-includes/ms-files.php, load-scripts.php). Direct access to these files should be restricted.

How to Fix

Block direct PHP access to wp-includes

Add to your main .htaccess (outside the WordPress BEGIN/END block):
RewriteEngine On
RewriteRule ^wp-includes/[^/]+\.php$ - [F,L]
RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L]
RewriteRule ^wp-includes/theme-compat/ - [F,L]

Exception for allowed wp-includes files

Certain files like wp-includes/js/tinymce/wp-tinymce.php may be needed. Test your site after adding the rule and whitelist any that legitimately need to be accessible.

Verify the fix:

Visit https://yourdomain.com/wp-includes/class-wp.php — it should return 403 Forbidden.

Run a Free Scan Verify this issue on your site

Your WordPress site has multiple administrator accounts. Each admin account is an additional attack surface — if any one is compromised, the attacker has full site control. Use the principle of least privilege: give users only the permissions they actually need.

How to Fix

Audit all administrator accounts

Go to Users → All Users and filter by Role: Administrator. For each admin account, verify it belongs to an active person who genuinely needs admin access.

Downgrade unnecessary admin accounts

For users who only write posts, change their role to Editor or Author. For users who need no login access, remove the account entirely. Click the username → Edit → Role → Change and Update.

Enable 2FA for all remaining admins

Use the Two Factor plugin to require 2FA for all administrator accounts. See the 2FA guide in this knowledge base.

Verify the fix:

Users → All Users → Filter by Administrator. You should see only the minimum necessary admin accounts, all with recognizable names and active email addresses.

Run a Free Scan Verify this issue on your site

Automatic background updates for WordPress core are disabled. WordPress releases security patches frequently — sometimes for critical vulnerabilities being actively exploited. Manual-only updates mean your site stays vulnerable until someone remembers to update it.

How to Fix

Enable automatic core security updates

In wp-config.php:
define('WP_AUTO_UPDATE_CORE', 'minor');
This enables automatic minor and security releases (4.x.1, 4.x.2) but not major version jumps.

Enable automatic plugin updates

In wp-config.php:
add_filter('auto_update_plugin', '__return_true');
Or enable per-plugin in the Plugins list by clicking "Enable auto-updates" for each one.

Use ManageWP or MainWP for managed updates

If you manage multiple WordPress sites, tools like ManageWP or MainWP let you manage and apply updates across all sites from a single dashboard, with backup-before-update options.

Verify the fix:

Check Dashboard → Updates regularly. Consider installing Easy Updates Manager plugin for granular control over what updates automatically.

Run a Free Scan Verify this issue on your site

Your WordPress comment section has no spam protection. Unprotected comment forms are targeted by bots that submit thousands of spam comments containing malicious links, which can harm your SEO, expose your users to malware links, and consume your moderation time.

How to Fix

Enable Akismet Anti-Spam

Akismet comes bundled with WordPress. Go to Plugins → Installed Plugins, activate Akismet, and get a free API key at akismet.com (free for personal use).

Require comment approval

Go to Settings → Discussion and enable Comment must be manually approved. Also enable Comment author must have a previously approved comment to whitelist known good commenters.

Add honeypot or CAPTCHA to comment form

Install Antispam Bee (free) which adds an invisible honeypot field to the comment form. Bots that fill in hidden fields are automatically rejected without CAPTCHA friction for real users.

Verify the fix:

Check Comments → Spam — Akismet or Antispam Bee should be catching and quarantining spam. Submit a test comment from a fresh browser and confirm it's held for moderation.

Run a Free Scan Verify this issue on your site
Server Disclosure

Your server's HTTP response includes the exact software version (e.g., Apache/2.4.51), allowing attackers to search for known exploits for that specific version.

How to Fix

Apache: suppress version

Edit /etc/apache2/apache2.conf (or httpd.conf) and add:
ServerTokens Prod
ServerSignature Off

Nginx: hide version

In nginx.conf inside the http {} block:
server_tokens off;

Cloudflare / CDN users

If you use Cloudflare, enable Security Level and use Transform Rules to remove the Server header entirely.

Verify the fix:

Run curl -I https://yourdomain.com — the Server header should say Apache or nginx without a version number, or be absent entirely.

Run a Free Scan Verify this issue on your site

The X-Powered-By header reveals your backend language and framework version (e.g., PHP/8.1.2), making targeted attacks easier.

How to Fix

PHP: disable in php.ini

Find and edit php.ini (run php --ini to find it) and set:
expose_php = Off
Then restart your web server.

Apache: remove header

Header unset X-Powered-By
Requires mod_headers enabled.

Nginx: remove header

proxy_hide_header X-Powered-By;
fastcgi_hide_header X-Powered-By;

Verify the fix:

curl -I https://yourdomain.com should no longer include x-powered-by.

Run a Free Scan Verify this issue on your site

While robots.txt is intentionally public, listing internal paths (admin areas, API endpoints, backup directories) in Disallow rules inadvertently advertises those paths to attackers who specifically look here.

How to Fix

Audit your Disallow entries

Review each Disallow: line. Remove entries that reveal sensitive infrastructure paths. Protecting a path from crawlers is not security — anyone can read robots.txt.

Protect the actual resources

Instead of hiding paths in robots.txt, protect the real resources: require authentication, add IP restrictions, or block access in .htaccess / Nginx config.

Use a single generic disallow

A common minimal approach: User-agent: *\nDisallow: /wp-admin/ — this is already public knowledge for WordPress sites and avoids revealing custom paths.

Verify the fix:

Review https://yourdomain.com/robots.txt and confirm no sensitive internal paths are listed.

Run a Free Scan Verify this issue on your site

The Apache /server-status endpoint is publicly accessible. This page reveals real-time server metrics including active connections, request URIs currently being processed (which may contain session tokens or sensitive URLs), worker load, and server uptime — providing attackers with a real-time intelligence feed about your server.

How to Fix

Restrict or disable server-status

In your Apache config or .htaccess:
<Location "/server-status">
Require ip 127.0.0.1
Require ip YOUR.OFFICE.IP
</Location>

Or disable mod_status entirely: sudo a2dismod status && sudo systemctl restart apache2

Block via Nginx if proxying Apache

Add a location block:
location /server-status {
deny all;
return 404;
}

Also check /server-info

The /server-info endpoint reveals loaded Apache modules, configuration directives, and compile-time settings. Apply the same IP restriction or disable mod_info.

Verify the fix:

Visit https://yourdomain.com/server-status from an external IP — it should return 403 or 404.

Run a Free Scan Verify this issue on your site

PHP errors, warnings, or notices are being output directly to your web pages. These messages reveal your file system paths, database table names, function names, variable contents, and code structure — giving attackers a detailed map of your application internals.

How to Fix

Disable error display in php.ini

Edit php.ini and set:
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php-errors.log

Restart Apache or PHP-FPM after changes.

Override via .htaccess (if no php.ini access)

Add to your .htaccess:
php_flag display_errors Off
php_flag log_errors On

WordPress-specific suppression

In wp-config.php:
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', 0);

Verify the fix:

Trigger a page error (visit a malformed URL) and confirm no PHP warning/notice text appears. Check your error log file instead: sudo tail -f /var/log/php-errors.log.

Run a Free Scan Verify this issue on your site

Files like CHANGELOG.md, CHANGES.txt, HISTORY.md, or RELEASE_NOTES.txt are publicly accessible in your webroot. These files reveal your software version history, dependencies used, and sometimes details about security fixes that were applied — helping attackers identify what vulnerabilities previously existed.

How to Fix

Block common changelog filenames in Apache

Add to .htaccess:
<FilesMatch "^(CHANGELOG|CHANGES|HISTORY|RELEASE_NOTES|UPGRADING)(\.md|\.txt|\.rst)?$">
Deny from all
</FilesMatch>

Block in Nginx

location ~* ^/(CHANGELOG|CHANGES|HISTORY|RELEASE_NOTES)(\.md|\.txt|\.rst)?$ {
deny all;
return 404;
}

Remove from webroot via deployment

Best practice is to exclude documentation files from your deployment entirely. In your CI/CD pipeline or rsync command:
rsync --exclude="CHANGELOG*" --exclude="*.md" src/ dest/

Verify the fix:

Visit https://yourdomain.com/CHANGELOG.md — it should return 403 or 404.

Run a Free Scan Verify this issue on your site

Cloudflare Development Mode is enabled. In this mode, Cloudflare bypasses its cache and security features and serves content directly from your origin server. Your origin server's real IP address may be exposed to visitors, and performance optimizations are disabled. Dev Mode auto-disables after 3 hours but should be turned off manually when testing is complete.

How to Fix

Disable Development Mode in Cloudflare

Log into Cloudflare Dashboard → select your domain → Caching → Configuration → Development Mode → toggle Off.

Protect your origin server IP

Configure your origin server's firewall to only accept HTTP/HTTPS connections from Cloudflare's published IP ranges. Block all other sources on ports 80/443. This prevents attackers from bypassing Cloudflare to reach your origin directly.

Use the Cache Purge feature instead

If you're purging cache during testing, use Caching → Purge Cache → Purge Everything instead of enabling Dev Mode. This clears cached content without disabling security.

Verify the fix:

Visit https://yourdomain.com and check the cf-cache-status response header — it should show HIT or MISS, not an absent header (which indicates Dev Mode bypass).

Run a Free Scan Verify this issue on your site
Content & Cookies

Your HTTPS page loads some resources (images, scripts, or stylesheets) over HTTP. This breaks encryption, triggers browser warnings, and may cause resources to be blocked.

How to Fix

Use Really Simple SSL plugin

The plugin auto-corrects HTTP URLs in content, options, and the database on activation.

Search-replace HTTP URLs in database

Use Better Search Replace plugin to replace all http://yourdomain.com with https://yourdomain.com in the database.

Add upgrade-insecure-requests CSP directive

As a fallback, add to your CSP: Content-Security-Policy: upgrade-insecure-requests — this tells browsers to upgrade HTTP resource requests to HTTPS automatically.

Verify the fix:

Open browser DevTools → Console. There should be no "Mixed Content" warnings after loading your page.

Run a Free Scan Verify this issue on your site

An open directory listing allows anyone to browse your server file structure and download files — including backups, config files, and private data.

How to Fix

Apache: disable directory indexing

Add to .htaccess (or main server config):
Options -Indexes

Nginx: disable autoindex

In your server block: autoindex off;

Add index files to all directories

Place a blank index.php file (just <?php // silence) inside directories that should not be browsable.

Verify the fix:

Visit the directory URL that was flagged — it should return 403 Forbidden, not a file listing.

Run a Free Scan Verify this issue on your site

Your server sends Access-Control-Allow-Origin: * for all requests, including API endpoints. This allows any website on the internet to make cross-origin requests to your API and read the responses. For authenticated APIs, this can leak private user data.

How to Fix

Restrict CORS to specific origins (Apache)

Replace the wildcard with an allowlist:
SetEnvIf Origin "^https://(yourdomain\.com|app\.yourdomain\.com)$" CORS_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%{CORS_ORIGIN}e" env=CORS_ORIGIN

Restrict CORS in Nginx

set $cors_origin "";
if ($http_origin ~* "^https://(yourdomain\.com|app\.yourdomain\.com)$") {
set $cors_origin $http_origin;
}
add_header Access-Control-Allow-Origin $cors_origin;

WordPress REST API CORS restriction

Add to functions.php:
add_action('rest_api_init', function() {
remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
add_filter('rest_pre_serve_request', function($value) {
$allowed = ['https://yourdomain.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed)) {
header('Access-Control-Allow-Origin: ' . $origin);
}
return $value;
});
});

Verify the fix:

Use curl -H "Origin: https://evil.com" -I https://yourdomain.com/wp-json/wp/v2/posts — the response should NOT include access-control-allow-origin: https://evil.com.

Run a Free Scan Verify this issue on your site

WordPress authentication cookies (logged-in session tokens) are valid for 2 days (non-remembered) or 14 days (remembered) by default. For admin accounts especially, indefinitely long sessions increase the window of opportunity for session hijacking or theft of an unattended device.

How to Fix

Shorten session duration via filter

Add to functions.php:
add_filter('auth_cookie_expiration', function($expiration, $user_id, $remember) {
return $remember ? (7 * DAY_IN_SECONDS) : (4 * HOUR_IN_SECONDS);
}, 10, 3);

Force logout on browser close

Ensure the "Remember Me" checkbox is not pre-checked on the login form. Add to functions.php:
add_filter('login_footer', function() {
echo '<script>document.getElementById("rememberme").checked=false;</script>';
});

Use Solid Security idle session logout

Solid Security includes an "Away Mode" and idle session logout feature under Security → Settings → User Groups → Logout Idle Users.

Verify the fix:

Log in without "Remember Me", wait beyond the expected expiry period, and confirm the session has ended.

Run a Free Scan Verify this issue on your site

User passwords, session tokens, API keys, or personally identifiable information are being passed as URL query parameters (e.g., ?token=abc123). URLs are stored in browser history, server access logs, CDN logs, and Referrer headers — exposing sensitive data to multiple unintended parties.

How to Fix

Move sensitive parameters to POST body or headers

Never transmit secrets via GET parameters. Use HTTP POST requests with the data in the request body, or pass API keys via Authorization headers: Authorization: Bearer your-token-here

Audit your forms and links

Search your codebase for $_GET['password'], $_GET['token'], $_GET['key']. Replace with $_POST or header-based retrieval.

Configure Referrer-Policy header

Add Referrer-Policy: no-referrer to prevent URL parameters from being sent to third-party sites in Referrer headers if you cannot immediately fix the URL issue.

Verify the fix:

Review your site's access logs for patterns like ?password=, ?token=, or ?key= appearing in logged URLs.

Run a Free Scan Verify this issue on your site

Other websites can embed your images directly, consuming your server bandwidth and hosting costs without your permission. Heavy hotlinking can cause significant bandwidth bills and slow down your site for actual visitors.

How to Fix

Block hotlinking in Apache (.htaccess)

Add to your .htaccess:
RewriteEngine on
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?yourdomain\.com/ [NC]
RewriteRule \.(jpg|jpeg|png|gif|webp|svg)$ - [F,NC,L]

Block hotlinking in Nginx

location ~* \.(jpg|jpeg|png|gif|webp)$ {
valid_referers none blocked yourdomain.com www.yourdomain.com;
if ($invalid_referer) { return 403; }
}

Use Cloudflare Hotlink Protection

In Cloudflare Dashboard, go to Scrape Shield → Hotlink Protection and toggle it on. This is the easiest one-click solution.

Verify the fix:

Try embedding one of your images on a different domain (e.g., via a temporary HTML file) — it should display a 403 error or broken image.

Run a Free Scan Verify this issue on your site

Your site has an open redirect — a URL parameter (like ?redirect= or ?url=) that accepts arbitrary destinations and redirects users there. Attackers exploit this to craft URLs that appear to be from your trusted domain but send users to phishing sites: yourdomain.com/login?redirect=evil.com.

How to Fix

Validate redirect destinations server-side

Never redirect to a user-supplied URL without validation. In PHP:
$allowed_hosts = ['yourdomain.com', 'www.yourdomain.com'];
$url = $_GET['redirect'] ?? '/';
$parsed = parse_url($url);
if (isset($parsed['host']) && !in_array($parsed['host'], $allowed_hosts)) {
$url = '/';
}
header('Location: ' . $url);

Use relative URLs for internal redirects

For login redirects, use relative paths like /dashboard rather than full URLs. Strip any http:// or https:// prefix before redirecting.

Audit WordPress redirect parameters

WordPress uses redirect_to in login forms. Wordfence and Solid Security validate this parameter. Ensure you're running a modern version of these plugins.

Verify the fix:

Try visiting https://yourdomain.com/wp-login.php?redirect_to=https://evil.com — after login, you should be redirected to your dashboard, not to evil.com.

Run a Free Scan Verify this issue on your site
Exposed Files

Your .env file is accessible from the internet. This file typically contains database credentials, API keys, secret tokens, and other sensitive configuration values in plain text. Any visitor can download it and take full control of your site and services.

How to Fix

Block .env in Apache (.htaccess)

Add to your .htaccess:
<Files ".env">
  Order allow,deny
  Deny from all
</Files>

Block .env in Nginx

Add to your server block:
location ~ /\.env {
  deny all;
  return 404;
}

Move .env above webroot

The safest approach is to move your .env file one level above your public web root (e.g., /var/www/.env instead of /var/www/html/.env). Update your app to read it from that path.

Rotate all exposed credentials

Assume all secrets in the file are compromised. Rotate: database password, API keys, JWT secrets, and any third-party service tokens immediately. Update your firewall to revoke old access.

Verify the fix:

Run curl -I https://yourdomain.com/.env — the response should be 403 Forbidden or 404 Not Found, not 200 OK.

Run a Free Scan Verify this issue on your site

Your .git directory is publicly accessible. This allows attackers to reconstruct your entire source code, view commit history (which may include credentials), and download all project files — even those not served to the public.

How to Fix

Block .git in Apache (.htaccess)

Add to your .htaccess:
RedirectMatch 404 /\.git
Or:
<DirectoryMatch "^\.git">
  Deny from all
</DirectoryMatch>

Block .git in Nginx

location ~ /\.git {
  deny all;
  return 404;
}

Remove .git from your webroot entirely

The git repository directory should not exist in your production webroot at all. Use a deployment tool (Deployer, Envoyer, CI/CD pipeline) that copies only production files — without the .git folder.

Scan git history for secrets

Run git log --all --full-history and tools like truffleHog or git-secrets to find any credentials ever committed to the repo. Rotate anything found.

Verify the fix:

curl -I https://yourdomain.com/.git/config must return 403 or 404.

Run a Free Scan Verify this issue on your site

A backup copy of your wp-config.php file (e.g., wp-config.php.bak, wp-config.php~, or wp-config.php.old) is publicly accessible. Unlike wp-config.php itself (which PHP parses and never outputs), backup files with different extensions are served as raw text — exposing your database credentials and secret keys.

How to Fix

Delete the backup file immediately

Use FTP or SSH to remove the file: rm /var/www/html/wp-config.php.bak. Check for any other variants: ls /var/www/html/wp-config*.

Block backup file extensions in Apache

<FilesMatch "wp-config\.php\.(bak|old|copy|orig|tmp|~)$">
  Deny from all
</FilesMatch>

Rotate your database credentials

Assume the database password and WordPress secret keys have been seen. Change your MySQL password, update wp-config.php with the new password, and regenerate your WordPress secret keys at WordPress secret key API.

Verify the fix:

curl -I https://yourdomain.com/wp-config.php.bak must return 403 or 404.

Run a Free Scan Verify this issue on your site

A database dump or archive backup file (e.g., backup.sql, database.sql, backup.zip) is publicly downloadable from your webroot. These files contain your entire database — user accounts, hashed passwords, private posts, and all configuration — which attackers can download and crack offline.

How to Fix

Remove backup files from webroot

Delete all .sql, .zip, .tar.gz backup files from your public web directory. Run: find /var/www/html -name "*.sql" -o -name "*backup*" -o -name "*dump*" | xargs rm.

Store backups outside the webroot

Always save backups to a directory that is NOT served by your web server. For example: /var/backups/ or a private S3 bucket with no public ACL.

Block common backup extensions

Add to Apache .htaccess:
<FilesMatch "\.(sql|bak|backup|dump|db)$">
  Order allow,deny
  Deny from all
</FilesMatch>

Rotate database passwords

Assume the dump has been downloaded. Change your database password and update wp-config.php. Force-reset all admin accounts in WordPress.

Verify the fix:

curl -I https://yourdomain.com/backup.sql should return 403 or 404.

Run a Free Scan Verify this issue on your site

A PHP info page (phpinfo.php or info.php) is publicly accessible. It outputs detailed information about your server: PHP version and configuration, installed extensions, all environment variables, server path, and loaded php.ini settings — giving attackers a complete map of your server configuration.

How to Fix

Delete the phpinfo file

Remove phpinfo.php and info.php from your webroot: rm /var/www/html/phpinfo.php

Block via Nginx or Apache if you cannot delete

Nginx: location ~ /(phpinfo|info)\.php { deny all; return 404; }
Apache: <Files "phpinfo.php"> Deny from all </Files>

Verify the fix:

curl -I https://yourdomain.com/phpinfo.php must return 403 or 404.

Run a Free Scan Verify this issue on your site

wp-content/debug.log is publicly accessible. WordPress writes detailed error information to this file when WP_DEBUG_LOG is enabled. It typically contains full file paths, database query errors, plugin stack traces, and other information that helps attackers map your system.

How to Fix

Block debug.log in .htaccess

Add to your .htaccess:
<Files "debug.log">
  Order allow,deny
  Deny from all
</Files>

Disable debug logging on production

In wp-config.php, ensure:
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);

Use a custom log path

If you need debug logging, move it outside the webroot:
define('WP_DEBUG_LOG', '/var/log/wordpress-debug.log');

Verify the fix:

curl -I https://yourdomain.com/wp-content/debug.log should return 403 or 404.

Run a Free Scan Verify this issue on your site

wp-admin/install.php is accessible on your live site. This file is only used during the initial WordPress installation. Leaving it accessible on a production site is unnecessary and could confuse security tools — block it.

How to Fix

Block install.php in .htaccess

<Files "install.php">
  Order allow,deny
  Deny from all
</Files>

Use Wordfence or Solid Security to block wp-admin access by IP

Restrict /wp-admin/ access to your IP address using your security plugin, or in .htaccess:
<Directory "/wp-admin">
  Order Deny,Allow
  Deny from all
  Allow from YOUR.IP.HERE
</Directory>

Verify the fix:

curl -I https://yourdomain.com/wp-admin/install.php should return 403.

Run a Free Scan Verify this issue on your site

.DS_Store is a macOS metadata file automatically created by Finder. When uploaded to your server by accident, it reveals your local directory structure, file names, and folder layout — helping attackers map hidden paths they wouldn't otherwise know exist.

How to Fix

Delete the file

Via FTP or SSH, delete .DS_Store from any public web directory. Check recursively: find /var/www/html -name ".DS_Store" -type f -delete

Block access in Apache (.htaccess)

<Files ".DS_Store">
Order allow,deny
Deny from all
</Files>

Block in Nginx

location ~ /\.DS_Store {
deny all;
return 404;
}

Prevent future uploads

Configure your deployment process (rsync, Git, FTP client) to exclude dot-files. In rsync: rsync --exclude=".DS_Store". In Git: add .DS_Store to your global ~/.gitignore.

Verify the fix:

curl -I https://yourdomain.com/.DS_Store should return 403 or 404.

Run a Free Scan Verify this issue on your site

composer.json is publicly accessible. It lists every PHP dependency your application uses, including exact version numbers. Attackers cross-reference these against known CVE databases to identify unpatched vulnerabilities in your stack.

How to Fix

Move composer files above webroot

The cleanest fix is to keep composer.json, composer.lock, and the vendor/ directory one level above your public web root so they're never web-accessible.

Block access in Apache (.htaccess)

<Files "composer.json">
Order allow,deny
Deny from all
</Files>
<Files "composer.lock">
Order allow,deny
Deny from all
</Files>

Block in Nginx

location ~* composer\.(json|lock)$ {
deny all;
return 404;
}

Verify the fix:

curl -I https://yourdomain.com/composer.json should return 403 or 404.

Run a Free Scan Verify this issue on your site

package.json is publicly accessible. It lists your JavaScript dependencies and their versions, which attackers use to find known CVEs in your Node.js or frontend toolchain. It may also reveal internal scripts and project structure.

How to Fix

Block access in Apache (.htaccess)

<Files "package.json">
Order allow,deny
Deny from all
</Files>
<Files "package-lock.json">
Order allow,deny
Deny from all
</Files>

Block in Nginx

location ~* package(-lock)?\.json$ {
deny all;
return 404;
}

Keep build files out of webroot

Configure your build process so that package.json and node_modules/ stay in your build directory, not in the public web root. Only ship compiled/bundled assets.

Verify the fix:

curl -I https://yourdomain.com/package.json should return 403 or 404.

Run a Free Scan Verify this issue on your site

If your site is fronted by a reverse proxy (Nginx, CDN, or load balancer) without proper rules, .htaccess files may be served as plain text, revealing your security rules, redirect logic, password-protected directory structure, and server path information.

How to Fix

Block dot-files in Nginx reverse proxy

In your Nginx config:
location ~ /\. {
deny all;
return 404;
}

This blocks all dot-files and dot-directories from being served.

Block in Apache as fallback

Apache natively blocks .htaccess by default. Confirm via AllowOverride None in the global config and no override for <Files ".ht*">.

Test all dot-files

Check curl -I https://yourdomain.com/.htaccess, /.htpasswd, and /.env — all should return 403 or 404.

Verify the fix:

curl -I https://yourdomain.com/.htaccess should return 403 or 404, never 200 with text content.

Run a Free Scan Verify this issue on your site

Temporary files created by text editors (Vim .swp, Emacs ~ backups, nano temp files) are publicly accessible in your webroot. These files contain your source code including credentials, database connection strings, and server-side logic that would otherwise be parsed and hidden by PHP.

How to Fix

Find and delete swap/temp files

Search for them: find /var/www/html -name "*.swp" -o -name "*~" -o -name "*.tmp" | xargs ls -la
Delete found files: find /var/www/html -name "*.swp" -o -name "*~" -delete

Block temp file extensions in Apache

<FilesMatch "(\.(swp|swo|bak|orig|tmp)|~)$">
Deny from all
</FilesMatch>

Block in Nginx

location ~* (\.(swp|swo|bak|orig|tmp)|~)$ {
deny all;
return 404;
}

Configure your editor to save swaps elsewhere

In Vim, add to ~/.vimrc: set directory=$HOME/.vim/swaps// to store swap files in a non-webroot directory.

Verify the fix:

Create a test file: touch /var/www/html/test.swp, then run curl -I https://yourdomain.com/test.swp — it should return 403. Delete the test file afterward.

Run a Free Scan Verify this issue on your site

A database management tool (Adminer, phpMyAdmin, or similar) is publicly accessible on your domain. These tools provide a web interface to your database — anyone who finds it can attempt to brute-force your database credentials, and known vulnerabilities in these tools have been exploited widely.

How to Fix

Remove from webroot if not needed

Delete adminer.php, phpmyadmin/, or similar directories from your public webroot. Database management should never be done from a public URL on production.

Restrict by IP if you must keep it

In Apache .htaccess:
<Files "adminer.php">
Order Deny,Allow
Deny from all
Allow from YOUR.OFFICE.IP
</Files>

Use SSH tunneling for database access

The secure way to manage databases remotely is SSH port forwarding: ssh -L 3306:127.0.0.1:3306 user@yourserver, then connect via TablePlus, Sequel Pro, or DBeaver on 127.0.0.1:3306.

Verify the fix:

Visit https://yourdomain.com/adminer.php and https://yourdomain.com/phpmyadmin/ — both should return 403 or 404.

Run a Free Scan Verify this issue on your site

A server or application error log file (e.g., error.log, access.log, php_errors.log) is publicly accessible in your webroot. Error logs contain stack traces, full file paths, database errors, SQL queries, and sometimes even submitted user data from failed requests.

How to Fix

Move log files outside the webroot

The correct location for log files is outside the public directory, such as /var/log/apache2/, /var/log/nginx/, or /var/log/app-name/. Update your app config to write logs there.

Block log file access in Apache

<FilesMatch "\.(log|txt)$">
Order allow,deny
Deny from all
</FilesMatch>

Be careful not to block legitimate .txt files if your site needs them — use specific filenames instead.

Block in Nginx

location ~* \.(log)$ {
deny all;
return 404;
}

Verify the fix:

curl -I https://yourdomain.com/error.log should return 403 or 404.

Run a Free Scan Verify this issue on your site
Malware & Backdoors

Obfuscated JavaScript patterns (such as eval(base64_decode()), eval(unescape()), or eval(atob())) were found in your page source. Legitimate code is almost never written this way on purpose — this is a strong indicator that malicious code has been injected into your site, likely by a compromised plugin, theme, or file.

How to Fix

Use a malware scanner immediately

Install Wordfence Security (free) or run a scan via Sucuri SiteCheck (sucuri.net/website-scanner). These tools identify modified files and malware injections.

Compare files against clean backup

Download your current site files and compare them against a clean WordPress installation and your last known-good backup using diff -r /clean/ /infected/ or a tool like WinMerge.

Check recently modified files

Run via SSH: find /var/www/html -name "*.php" -newer /var/www/html/wp-config.php -type f to find files modified after your last clean install.

Restore from a clean backup and harden

After identifying the injected files, restore from a verified clean backup. Then: update all plugins/themes/core, change all passwords, and remove unused plugins. Contact your host to request a server-level malware scan.

Verify the fix:

After cleanup, re-scan with Sucuri SiteCheck and Wordfence. Confirm no obfuscated code appears in page source via browser DevTools → View Source.

Run a Free Scan Verify this issue on your site

Hidden iframes with zero dimensions (width=0, height=0, or display:none) were detected in your page source. This is a classic drive-by malware technique — an invisible iframe silently loads a malicious page that attempts to exploit visitors' browsers to install malware, steal cookies, or redirect to phishing sites.

How to Fix

Identify and remove the injection

Use browser DevTools (F12) → Elements to find the hidden iframe. Note the src URL. Search your theme files and database for this string: grep -r "iframe" /var/www/html/wp-content/ | grep -i "display:none\|width=0"

Scan database for injected code

Run this WP-CLI command to search your database:
wp db query "SELECT ID,post_title FROM wp_posts WHERE post_content LIKE '%iframe%';"
Also check widget options and theme mods.

Run a full malware scan

Use Wordfence or Sucuri to perform a full file and database scan. These tools identify injected content in both files and the WordPress database.

Restore clean files and change all credentials

Restore affected theme/plugin files from clean backups. Change your WordPress admin password, database password, and all API keys. Update all plugins and themes.

Verify the fix:

View page source (Ctrl+U) and search for "iframe". No hidden iframes should exist.

Run a Free Scan Verify this issue on your site

JavaScript files are being loaded from external domains with high-risk TLDs (.tk, .ml, .xyz, etc.) commonly associated with malware distribution. Legitimate CDN-hosted scripts come from known providers (cdnjs.cloudflare.com, cdn.jquery.com, etc.). Unrecognized external scripts can steal data, inject ads, or serve malware to your visitors.

How to Fix

Identify the source of the script tag

Search your theme files and plugins:
grep -r "script src" /var/www/html/wp-content/ | grep -v "cdnjs\|jquery\|googleapis\|cloudflare"
Also check the database: wp option get widget_text

Remove the suspicious script

Delete or clean the file containing the injected script. If it's in a plugin or theme file that should not contain that URL, restore from a clean backup.

Implement a Content Security Policy (CSP)

A CSP header explicitly whitelists which domains can load scripts on your site. Any unwhitelisted external script will be blocked by the browser. See the CSP solution guide in this knowledge base.

Verify the fix:

View page source and check all <script src= tags. Every domain should be recognizable (your domain, major CDNs, or known services).

Run a Free Scan Verify this issue on your site

Spam keywords (pharmacy, casino, or financial spam) were detected in your page source. Hackers inject hidden spam links to improve rankings for their sites on yours. These links may be hidden from normal visitors via CSS but visible to search engines, resulting in Google penalties and deindexing.

How to Fix

Find the injected content

Search your database for spam content:
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%viagra%' OR post_content LIKE '%casino%';"
Also check footer widgets and theme options.

Scan for PHP backdoors

SEO spam is usually inserted by a PHP backdoor. Run: find /var/www/html -name "*.php" | xargs grep -l "base64_decode\|eval\|gzuncompress" to find suspicious files.

Request Google Search Console review

After cleaning, submit your site for a Manual Actions review in Google Search Console. This lifts any spam penalties and restores rankings.

Verify the fix:

Use curl https://yourdomain.com | grep -i "viagra\|casino\|payday" — should return no matches after cleanup.

Run a Free Scan Verify this issue on your site

A <meta http-equiv="refresh"> tag was found redirecting visitors to an external domain. Legitimate sites almost never use this technique. It is a classic malware-injected redirect used to silently send visitors to phishing pages or malware-serving sites without their knowledge.

How to Fix

Find the injected tag in files

Search your theme and plugin files:
grep -r "http-equiv" /var/www/html --include="*.php" -l
Also check header.php, functions.php, and any recently modified files.

Check the database

The redirect may be stored in WordPress options. Use WP-CLI to search:
wp db query "SELECT option_name, option_value FROM wp_options WHERE option_value LIKE '%http-equiv%';"
Or use the Better Search Replace plugin to scan all tables.

Remove the redirect and do a full scan

Delete the <meta http-equiv="refresh"> line from whichever file or database row contains it. The redirect is a symptom — run Wordfence or Sucuri SiteCheck to find the root compromise, then rotate all credentials.

Verify the fix:

Run curl https://yourdomain.com | grep -i "http-equiv" — no meta refresh tag pointing to an external domain should appear.

Run a Free Scan Verify this issue on your site

A known malware signature (such as WP-VCD, C99shell, R57shell, or a PHP code-execution function) was detected in your page output. Your site has been compromised. Take it offline immediately to protect your visitors and begin incident response.

How to Fix

Take the site offline

Enable maintenance mode or add deny from all to your .htaccess to protect visitors while you clean.

Contact your hosting provider

Notify your host immediately. Many hosts offer emergency malware removal. Request a server-level scan and check for other compromised accounts on the same server.

Restore from a clean pre-infection backup

If you have a pre-infection backup (via UpdraftPlus, BlogVault, or your host), restore it. Verify the backup is clean before restoring.

Harden and relaunch

After restoring: change all passwords (WordPress admin, database, FTP, hosting panel), update all plugins/themes/core, install a WAF (Cloudflare or Sucuri), and enable two-factor authentication on admin accounts.

Verify the fix:

After cleanup, run Sucuri SiteCheck and Wordfence full scan. Both must show "no malware found".

Run a Free Scan Verify this issue on your site

A PHP webshell (a script that provides remote command execution via the browser) has been detected on your server. Common names include shell.php, c99.php, r57.php, wso.php. This means your server is fully compromised — an attacker can run arbitrary commands as your web server user.

How to Fix

Isolate the server immediately

If possible, take the site offline or restrict access. The attacker may have persistent access and could be monitoring their shell. Take a snapshot/backup of the compromised state for forensics before making changes.

Find all webshell files

Run: find /var/www/html -name "*.php" | xargs grep -l "system\|exec\|passthru\|shell_exec\|base64_decode" | grep -v "wp-includes\|wp-admin"
Also check: find /tmp /var/tmp -name "*.php"

Disable dangerous PHP functions

In php.ini:
disable_functions = system,exec,passthru,shell_exec,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
Restart PHP-FPM or Apache.

Block PHP execution in uploads directory

Add to .htaccess inside /wp-content/uploads/:
<Files *.php>
Deny from all
</Files>

This prevents uploaded PHP webshells from executing.

Verify the fix:

After cleanup, run Wordfence and Sucuri full scans. Confirm no PHP files exist in uploads: find /var/www/html/wp-content/uploads -name "*.php".

Run a Free Scan Verify this issue on your site

A browser-based cryptocurrency mining script (CoinHive, Coinhive successor, or similar) was detected in your page source. These scripts use your visitors' CPU resources to mine cryptocurrency without their knowledge or consent — causing browser slowdowns, battery drain, and legal liability under privacy laws.

How to Fix

Identify the injected script source

Search your WordPress files and database:
grep -r "coinhive\|cryptoloot\|minero\|coin-hive\|cryptonight" /var/www/html
Also check: wp db query "SELECT * FROM wp_options WHERE option_value LIKE '%coinhive%';"

Perform full malware remediation

A cryptominer implies full site compromise. Follow the known malware signature remediation steps: isolate, scan, restore from clean backup, rotate all credentials, update everything.

Implement a strict CSP

A strong Content Security Policy prevents unauthorized scripts from loading. Block all unknown domains in script-src to prevent re-injection even if the backdoor persists.

Verify the fix:

View page source and search for "coinhive", "cryptonight", "miner". Run Sucuri SiteCheck — it specifically checks for mining scripts.

Run a Free Scan Verify this issue on your site

API keys, secret tokens, or authentication credentials are exposed in your page's HTML source or JavaScript files. Anyone who views your source code can extract and use these keys — accessing third-party services, consuming your API quota, or incurring charges on your accounts.

How to Fix

Identify exposed keys in your source

View page source and search for patterns like API keys: grep -r "api_key\|apikey\|secret_key\|access_token\|AUTH_TOKEN" /var/www/html/wp-content/themes/ --include="*.js" --include="*.php"

Move secrets server-side

API calls involving secret keys should be made server-side (PHP), not client-side (JavaScript). Pass only the minimum required public identifier to the front-end (e.g., a Stripe publishable key is OK; a Stripe secret key is never sent to the browser).

Revoke and rotate all exposed keys immediately

Log into each affected third-party service (Google Maps, Stripe, Twilio, etc.) and revoke/regenerate the exposed API key. Update your server configuration with the new key. Apply IP restrictions or referer restrictions on the new key where possible.

Verify the fix:

View page source and search for "key", "secret", "token", "password". Verify no secret credentials appear in rendered HTML or linked JavaScript files.

Run a Free Scan Verify this issue on your site
DNS Records

Certification Authority Authorization (CAA) records tell certificate authorities which ones are permitted to issue SSL certificates for your domain. Without it, any CA can issue a certificate for you — enabling certificate hijacking.

How to Fix

Add a CAA record in your DNS

Log into your DNS provider and add a CAA record:
yourdomain.com. CAA 0 issue "letsencrypt.org"
Replace with your CA (e.g., digicert.com, sectigo.com).

Allow wildcard certificates too

Add a second record:
yourdomain.com. CAA 0 issuewild "letsencrypt.org"

Verify the fix:

Run dig CAA yourdomain.com or use MXToolbox CAA Lookup.

Run a Free Scan Verify this issue on your site

Without DNSSEC, attackers can forge DNS responses and redirect your visitors to malicious servers without anyone noticing (DNS poisoning / cache poisoning).

How to Fix

Enable DNSSEC at your registrar

Most registrars (Cloudflare, Namecheap, GoDaddy) offer DNSSEC in their DNS settings panel. Enable it with one click — they handle the key generation automatically.

Enable DNSSEC at your DNS host

If you use a separate DNS provider (e.g., Route 53, Cloudflare), enable DNSSEC there first, then add the DS record at your registrar.

Verify the fix:

Use Verisign DNSSEC Analyzer or run dig +dnssec yourdomain.com and look for the AD flag.

Run a Free Scan Verify this issue on your site

No IPv6 AAAA DNS records were found. IPv4 still works fine for most users, but IPv6 is increasingly required on modern mobile networks and is part of future-proofing your infrastructure.

How to Fix

Check if your host supports IPv6

Log into your hosting control panel (cPanel, Cloudflare, etc.) and check if an IPv6 address is assigned to your server. Many shared hosts now provide one by default.

Add an AAAA record

In your DNS manager, add a new record:
Type: AAAA, Name: @ (and www), Value: your server's IPv6 address.

Test connectivity

Use ipv6-test.com to confirm your site is reachable over IPv6 after DNS propagation.

Verify the fix:

Run dig AAAA yourdomain.com — you should see an IPv6 address in the answer section.

Run a Free Scan Verify this issue on your site

Your authoritative nameservers are publicly visible (as expected by DNS), but zone transfers (AXFR) must be restricted. If AXFR is allowed from any IP, an attacker can download your entire DNS zone, revealing every subdomain and internal hostname.

How to Fix

Restrict zone transfers (AXFR)

In BIND (named.conf):
zone "yourdomain.com" {
type master;
allow-transfer { none; };
};

For Cloudflare or Route53 users, zone transfers are disabled by default.

Use a reputable managed DNS provider

Cloudflare, AWS Route53, and Google Cloud DNS all block AXFR by default and offer DDoS-resilient anycast DNS as a bonus.

Verify using dig

Test: dig @ns1.yourdomain.com yourdomain.com AXFR
You should see Transfer failed or REFUSED, not a list of records.

Verify the fix:

dig @ns1.yourdomain.com yourdomain.com AXFR should return Transfer failed.

Run a Free Scan Verify this issue on your site

A DNS record points to a cloud service (GitHub Pages, Heroku, AWS S3, Netlify, etc.) that is no longer claimed. An attacker can register the matching service account and serve content under your subdomain — enabling phishing, cookie theft, and CSP bypass attacks against your users.

How to Fix

Identify dangling DNS records

Audit your DNS for CNAME records pointing to: *.github.io, *.herokuapp.com, *.s3.amazonaws.com, *.netlify.app, *.azurewebsites.net. For each, verify the target is still claimed and active.

Remove unused CNAME records

Log into your DNS provider and delete any CNAME records pointing to decommissioned services. If you no longer use a subdomain at all, delete the entire record.

Re-claim the service if still needed

If you still use the service, re-provision it (e.g., re-create the Heroku app or GitHub Pages repo) and verify the CNAME target is properly claimed before anyone else does.

Verify the fix:

Run dig CNAME subdomain.yourdomain.com for each subdomain. For any CNAME that resolves, visit the target URL and confirm it's your content — not a "404 - this page could not be found" from the cloud provider.

Run a Free Scan Verify this issue on your site

No reverse DNS (PTR) record exists for your server's IP address. Many mail servers reject or heavily penalise email from IPs without valid reverse DNS, contributing to your messages landing in spam. It also affects trust signals for your server IP in security tools and threat intelligence platforms.

How to Fix

Contact your hosting provider

PTR records are controlled by the IP owner (your hosting provider), not your domain registrar. Log a support ticket asking them to set a reverse DNS record for your IP pointing to mail.yourdomain.com or yourdomain.com.

Cloud providers: set rDNS in your control panel

In DigitalOcean: Droplet Settings → Edit. In Linode: Linodes → Network → Reverse DNS. In AWS: submit a request via the EC2 console → Elastic IPs → Actions → Update Reverse DNS.

Ensure forward and reverse DNS match

The hostname in your PTR record must have an A record pointing back to the same IP. Mismatched forward/reverse DNS is itself a spam signal. Verify: dig -x YOUR.SERVER.IP and dig A the-hostname-returned.

Verify the fix:

Run dig -x YOUR.SERVER.IP.HERE — the answer should return a valid hostname pointing back to your domain.

Run a Free Scan Verify this issue on your site
Email Security

Without SPF, anyone can send email pretending to be from your domain. This is used in phishing attacks that impersonate your brand to deceive customers or employees.

How to Fix

Find your mail provider's SPF include

Your email host (e.g., Google Workspace, Microsoft 365) provides an SPF include string. For Google: include:_spf.google.com. For M365: include:spf.protection.outlook.com.

Add a TXT record to your DNS

Go to your DNS provider and add a TXT record for @ (root domain):
v=spf1 include:_spf.google.com -all
The -all means hard fail — reject anything not listed.

Avoid too many lookups

SPF records support max 10 DNS lookups. If you use many services, flatten the record using SPF Surveyor.

Verify the fix:

Run dig TXT yourdomain.com and look for the SPF record, or use MXToolbox SPF Check.

Run a Free Scan Verify this issue on your site

DMARC builds on SPF and DKIM to tell receiving mail servers what to do with emails that fail authentication. Without it, phishing emails that fail SPF/DKIM still get delivered.

How to Fix

Start with a monitoring policy

Add a TXT record for _dmarc.yourdomain.com:
v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com
This collects reports without blocking anything yet.

Tighten to quarantine then reject

After reviewing reports for 2–4 weeks, upgrade to p=quarantine (spam folder), then p=reject (block completely).

Use a DMARC report analyzer

Services like DMARC Analyzer, Postmark, or Dmarcian parse your reports and show you which sending sources fail.

Verify the fix:

Run dig TXT _dmarc.yourdomain.com or check with MXToolbox DMARC.

Run a Free Scan Verify this issue on your site

DKIM cryptographically signs outgoing emails so receivers can verify they weren't altered in transit. Without it, emails can be forged or modified, and DMARC enforcement is weakened.

How to Fix

Enable DKIM in your email provider

Google Workspace: Admin console → Apps → Gmail → Authenticate Email. Microsoft 365: Security → Policies → DKIM. Both generate a key and give you a DNS record to add.

Add the DNS TXT record

Your provider will give you a TXT record for selector._domainkey.yourdomain.com. Add it in your DNS panel and wait up to 48h for propagation.

Verify the fix:

Send an email from your domain to a Gmail address and click the ⋮ menu → Show original — look for DKIM: PASS.

Run a Free Scan Verify this issue on your site

MTA-STS forces other mail servers to use TLS when delivering email to your domain. Without it, email delivery can be downgraded to unencrypted connections.

How to Fix

Create the policy file

Host a file at https://mta-sts.yourdomain.com/.well-known/mta-sts.txt:
version: STSv1
mode: enforce
mx: mail.yourdomain.com
max_age: 86400

Add DNS record

Add a TXT record: _mta-sts.yourdomain.comv=STSv1; id=20240101000000Z
Update the id whenever you change the policy.

Verify the fix:

Visit https://mta-sts.yourdomain.com/.well-known/mta-sts.txt — it should return your policy file.

Run a Free Scan Verify this issue on your site

Brand Indicators for Message Identification (BIMI) allows your company logo to appear in email clients (Gmail, Apple Mail, Yahoo) next to your authenticated emails. Requires DMARC enforcement first. It increases brand recognition and trust with email recipients.

How to Fix

Ensure DMARC is at p=quarantine or p=reject first

BIMI requires an enforced DMARC policy. Set p=quarantine or p=reject in your _dmarc TXT record before proceeding.

Prepare an SVG Tiny 1.2 logo

Create a square SVG of your logo in SVG Tiny 1.2 format. Host it at a public HTTPS URL on your domain, e.g., https://yourdomain.com/logo_bimi.svg.

Add the BIMI DNS TXT record

Add a TXT record for default._bimi.yourdomain.com:
v=BIMI1; l=https://yourdomain.com/logo_bimi.svg;
For Gmail verified checkmarks, you also need a VMC (Verified Mark Certificate) from Entrust or DigiCert.

Verify the fix:

Run dig TXT default._bimi.yourdomain.com to confirm the record exists. Use BIMI Generator to validate your setup.

Run a Free Scan Verify this issue on your site

The _smtp._tls TXT record is missing. TLS Reporting (RFC 8460) instructs sending mail servers to report TLS connection failures when delivering email to your domain, helping you detect downgrade attacks and misconfigured MTA-STS policies.

How to Fix

Add the TLS-RPT DNS record

Add a TXT record for _smtp._tls.yourdomain.com:
v=TLSRPTv1; rua=mailto:tlsrpt@yourdomain.com
Replace the email with one you monitor. Reports are sent in JSON format daily.

Use a reporting service

Services like Mailhardener or Dmarcian accept and parse TLS-RPT reports alongside DMARC reports, giving you a unified dashboard.

Verify the fix:

Run dig TXT _smtp._tls.yourdomain.com — you should see the TLSRPTv1 record in the answer.

Run a Free Scan Verify this issue on your site

Administrator accounts are not protected with two-factor authentication (2FA). If an admin password is compromised via phishing, data breach, or brute force, an attacker gains full control of your site. 2FA is the single most effective control against account takeover.

How to Fix

Install the Two Factor plugin

Install Two Factor (by the WordPress core team) from the plugin repository. It supports TOTP (Google Authenticator, Authy), FIDO2 security keys, email codes, and backup codes.

Enforce 2FA for admin roles via Solid Security

Solid Security Pro lets you require 2FA for specific user roles and refuse login until 2FA is configured. Under Security → User Security → Two-Factor, set Requirement to Administrator.

Set up TOTP on your authenticator app

After installing, go to Users → Your Profile → Two-Factor Options. Enable "Time Based One-Time Password (TOTP)", scan the QR code with Google Authenticator or Authy, and save your backup codes in a secure location.

Verify the fix:

Log out and log back in as an admin. You should be prompted for a 2FA code before gaining access to the dashboard.

Run a Free Scan Verify this issue on your site
Open Ports

FTP transmits your username, password, and all file contents in plaintext. Any network observer can capture your credentials. Replace FTP with SFTP immediately.

How to Fix

Disable FTP, use SFTP instead

In cPanel, go to FTP Accounts → Configure FTP Client and switch to SFTP. In your FTP client (FileZilla, Cyberduck), change port 21 (FTP) to port 22 (SFTP).

Firewall port 21

Block the port at the firewall level:
sudo ufw deny 21
Or in your cloud provider's security group/firewall rules, remove the inbound rule for port 21.

Verify the fix:

Run nmap -p 21 yourdomain.com — the result should show filtered or closed.

Run a Free Scan Verify this issue on your site

Telnet sends everything including passwords in plaintext and has no security. Disable it immediately and use SSH instead.

How to Fix

Stop and disable the telnet service

sudo systemctl stop telnet
sudo systemctl disable telnet
sudo apt remove telnetd -y

Block at firewall

sudo ufw deny 23

Verify the fix:

nmap -p 23 yourdomain.com must show closed or filtered.

Run a Free Scan Verify this issue on your site

Your database is directly reachable from the internet. This is one of the most common causes of database breaches. Databases must only listen on localhost or a private network.

How to Fix

Bind MySQL to localhost only

Edit /etc/mysql/mysql.conf.d/mysqld.cnf and set:
bind-address = 127.0.0.1
Restart: sudo systemctl restart mysql

Block at firewall

sudo ufw deny 3306
In your cloud firewall/security group, remove any inbound rule allowing port 3306 from 0.0.0.0/0.

Verify the fix:

nmap -p 3306 yourdomain.com should show filtered.

Run a Free Scan Verify this issue on your site

An exposed Redis instance without authentication allows anyone to read, modify, or delete all cached data — including session tokens. This also enables remote code execution on many configurations.

How to Fix

Bind Redis to localhost

Edit /etc/redis/redis.conf:
bind 127.0.0.1
Restart: sudo systemctl restart redis

Set a strong password

In redis.conf:
requirepass YOUR_STRONG_PASSWORD_HERE

Block at firewall

sudo ufw deny 6379

Verify the fix:

redis-cli -h yourdomain.com ping should time out or be refused.

Run a Free Scan Verify this issue on your site

SSH on port 22 is constantly targeted by automated brute-force bots. While SSH itself is secure, misconfigured servers (allowing root login or password auth) are frequently compromised.

How to Fix

Disable root login and password auth

Edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Restart: sudo systemctl restart sshd

Change default port

Set Port 2222 (or any non-standard port) in sshd_config. This stops most automated scans, though security-by-obscurity is not a substitute for proper config.

Use fail2ban

sudo apt install fail2ban — automatically bans IPs after repeated failed login attempts.

Verify the fix:

Run sudo fail2ban-client status sshd to confirm protection is active.

Run a Free Scan Verify this issue on your site

MongoDB port 27017 is reachable from the internet. Thousands of MongoDB databases have been wiped and held for ransom due to this exact misconfiguration. Databases must never be internet-facing.

How to Fix

Bind MongoDB to localhost only

Edit /etc/mongod.conf:
net:
bindIp: 127.0.0.1

Restart: sudo systemctl restart mongod

Enable authentication

In mongod.conf:
security:
authorization: enabled

Create a dedicated user with minimum required permissions.

Block port at firewall level

sudo ufw deny 27017
Or use your cloud provider's security group / firewall rules to block 27017 from all external IPs.

Verify the fix:

Run nmap -p 27017 yourdomain.com from an external machine — the port should show filtered or closed.

Run a Free Scan Verify this issue on your site

PostgreSQL port 5432 is reachable from the internet. Databases must never be publicly accessible — all database connections should flow through the application server on localhost or a private network.

How to Fix

Bind PostgreSQL to localhost

Edit postgresql.conf (find it with sudo -u postgres psql -c "SHOW config_file;"):
listen_addresses = 'localhost'
Restart: sudo systemctl restart postgresql

Restrict in pg_hba.conf

Edit pg_hba.conf and ensure no entries allow connections from 0.0.0.0/0. Use 127.0.0.1/32 for local connections only.

Block at firewall

sudo ufw deny 5432 — apply immediately regardless of app config, as a second layer.

Verify the fix:

nmap -p 5432 yourdomain.com from an external host should show filtered or closed.

Run a Free Scan Verify this issue on your site

Port 8080 is open and reachable from the internet. This port is commonly used for development HTTP servers, alternative web servers, and admin panels — none of which should be publicly exposed on production.

How to Fix

Identify what is running

SSH into the server and run: sudo ss -tlnp | grep 8080 or sudo lsof -i :8080 to identify the process.

Stop the service if not needed

If it's a dev server, stop it: sudo systemctl stop <service-name> and disable it: sudo systemctl disable <service-name>

Block at firewall if intentional

If the service must run, restrict it: sudo ufw allow from YOUR.OFFICE.IP to any port 8080
Then sudo ufw deny 8080 to block all other IPs.

Verify the fix:

nmap -p 8080 yourdomain.com from outside should show filtered.

Run a Free Scan Verify this issue on your site

Port 8443 (an alternative HTTPS port) is reachable. This is often used for admin panels, control panels (cPanel, Plesk, Webmin), or secondary services. Verify this is intentional and that the service is properly secured.

How to Fix

Identify the service

Run: sudo ss -tlnp | grep 8443 to see what process is listening.

Ensure it is secured

If it's a control panel (Plesk, cPanel, Webmin), ensure it uses a valid TLS certificate, has 2FA enabled, and IP allowlisting configured.

Restrict access if possible

sudo ufw allow from YOUR.OFFICE.IP to any port 8443
sudo ufw deny 8443
This limits the panel to known office IPs only.

Verify the fix:

Confirm the service at port 8443 is expected and intentionally accessible. If not, block it at the firewall.

Run a Free Scan Verify this issue on your site

Port 25 is open on a web server. While mail servers legitimately use port 25, on a web server it often indicates an open relay — allowing spammers to route bulk email through your IP, leading to blacklisting and service disruption.

How to Fix

Verify if a mail service is intentionally running

Run sudo ss -tlnp | grep 25. If you are not running a mail server, identify and stop the service.

Block port 25 at the firewall

sudo ufw deny 25. Most cloud providers (AWS, GCP, Azure) block port 25 by default on new instances — verify this in your security group or firewall rules.

If using a mail server, restrict relay

Configure Postfix or Sendmail to only relay from authenticated users or localhost. In Postfix main.cf:
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated reject

Verify the fix:

nmap -p 25 yourdomain.com from an external host should show filtered or closed.

Run a Free Scan Verify this issue on your site

Memcached port 11211 is accessible from the internet. Memcached has no built-in authentication — anyone who can reach this port can read, write, or delete your entire cache. Exposed Memcached servers have also been weaponized for massive DDoS amplification attacks (up to 50,000x amplification factor).

How to Fix

Bind Memcached to localhost

Edit /etc/memcached.conf and set:
-l 127.0.0.1
Restart: sudo systemctl restart memcached

Block at firewall

sudo ufw deny 11211
Also block UDP 11211 specifically since DDoS amplification uses UDP:
sudo ufw deny proto udp to any port 11211

Enable SASL authentication (if multi-server)

If Memcached must be accessible on a private network, compile with SASL support and require authentication. However, binding to localhost is always the preferred approach for single-server setups.

Verify the fix:

nmap -p 11211 yourdomain.com should show filtered. Test UDP: nmap -sU -p 11211 yourdomain.com.

Run a Free Scan Verify this issue on your site

Elasticsearch port 9200 is reachable from the internet. Elasticsearch has no authentication by default. An attacker can read, modify, or delete all your indexed data via simple HTTP requests. Hundreds of millions of user records have been exposed this way.

How to Fix

Bind Elasticsearch to localhost

Edit /etc/elasticsearch/elasticsearch.yml:
network.host: 127.0.0.1
Restart: sudo systemctl restart elasticsearch

Enable X-Pack security (authentication)

In elasticsearch.yml:
xpack.security.enabled: true
Then run: bin/elasticsearch-setup-passwords auto to generate passwords for all built-in users.

Block at firewall immediately

sudo ufw deny 9200
Also block 9300 (Elasticsearch cluster transport port):
sudo ufw deny 9300

Verify the fix:

curl http://yourdomain.com:9200 from an external host should time out or be refused.

Run a Free Scan Verify this issue on your site

The Docker daemon API (port 2375) is reachable from the internet without TLS or authentication. This gives any remote attacker full root-equivalent control over your host — they can run containers, mount your filesystem, extract environment variables, and achieve complete server takeover in seconds.

How to Fix

Never expose the Docker socket or port 2375 publicly

Immediately block the port: sudo ufw deny 2375. If you need remote Docker access, use SSH tunneling instead: ssh -L 2375:localhost:2375 user@yourserver then set DOCKER_HOST=tcp://localhost:2375.

Restrict Docker daemon to Unix socket only

Edit /etc/docker/daemon.json to remove any "hosts": ["tcp://0.0.0.0:2375"] entry. The daemon should only listen on the Unix socket at unix:///var/run/docker.sock.

If remote access is needed, use TLS mutual auth

Configure Docker with TLS client certificates on port 2376. Generate certs with: openssl or Docker's built-in tools. Never use port 2375 (unauthenticated) in any environment.

Verify the fix:

curl http://yourdomain.com:2375/version from an external host must fail. nmap -p 2375,2376 yourdomain.com should show both ports filtered.

Run a Free Scan Verify this issue on your site

Remote Desktop Protocol (RDP) port 3389 is open to the internet. RDP is one of the most targeted attack vectors for ransomware delivery. Exposed RDP is exploited via brute-force, BlueKeep (CVE-2019-0708), and credential-stuffing attacks constantly.

How to Fix

Block RDP from public internet immediately

In your cloud firewall / Windows Firewall, remove the inbound rule for TCP 3389 from 0.0.0.0/0. Add an allow rule for only your specific static IP address.

Use a VPN for remote access instead

Set up a VPN (WireGuard, OpenVPN, or your cloud provider's VPN) and connect to the server over VPN. RDP should only be accessible on the internal VPN network, never directly from the internet.

Enable Network Level Authentication (NLA)

On Windows: System Properties → Remote → Allow connections only from computers running Remote Desktop with Network Level Authentication. NLA requires credentials before the session is established.

Verify the fix:

nmap -p 3389 yourdomain.com from an external network should show filtered or closed.

Run a Free Scan Verify this issue on your site

Even if you've disabled XML-RPC, the pingback feature specifically can be used for DDoS amplification — attackers send one request to your server instructing it to ping a victim URL, causing your server to unwillingly participate in a distributed attack. This can result in your server's IP being blacklisted.

How to Fix

Block system.multicall specifically

If you need XML-RPC for Jetpack, block only the multicall method that enables amplification:
add_filter('xmlrpc_methods', function($methods) {
unset($methods['system.multicall']);
unset($methods['pingback.ping']);
return $methods;
});

Block the X-Pingback header

Remove the header that advertises pingback capability:
add_filter('wp_headers', function($headers) {
unset($headers['X-Pingback']);
return $headers;
});

Use Cloudflare WAF rule to block XML-RPC abuse

In Cloudflare WAF, create a custom rule: if URI Path equals /xmlrpc.php AND Request Method equals POST AND IP is not your trusted IP → Block.

Verify the fix:

Test using: curl -d '<?xml version="1.0"?><methodCall><methodName>pingback.ping</methodName></methodCall>' https://yourdomain.com/xmlrpc.php — should return faultCode or 403.

Run a Free Scan Verify this issue on your site
🔒 SSL Secured
🛡️ SOC 2 Compliant
🌐 GDPR Ready
99.9% Uptime
🏆 ISO 27001