Step-by-step remediation guides for every vulnerability category our scanner detects. Find your issue below and follow the fix — no security expertise required.
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.
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
Install Really Simple SSL plugin and click Activate SSL, or add to wp-config.php:define('FORCE_SSL_ADMIN', true);
Certificates expire every 90 days. Run sudo certbot renew --dry-run to confirm auto-renewal is set. Most hosts handle this automatically.
Visit your site with https:// and check for the padlock icon. Use SSL Labs for a deep certificate report.
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.
Edit your SSL virtual host config and set:SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
Then restart: sudo systemctl restart apache2
In your nginx.conf or site config:ssl_protocols TLSv1.2 TLSv1.3;
Reload: sudo nginx -s reload
In WHM go to Service Configuration → Apache Configuration → Global Configuration and uncheck TLS 1.0 and 1.1 under SSL/TLS Protocols.
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.
Users who visit http:// are not automatically redirected to the secure https:// version. They browse unencrypted without knowing it.
Add to your .htaccess:RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Add a server block:server { listen 80; server_name yourdomain.com; return 301 https://$host$request_uri; }
The Really Simple SSL plugin handles this automatically, including fixing mixed content URLs.
Run curl -I http://yourdomain.com and confirm you see 301 Moved Permanently with a Location: https:// header.
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.
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
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;
Visit ssl-config.mozilla.org, select your server and profile (Modern/Intermediate), and copy the generated configuration directly.
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.
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.
Header always set Expect-CT "max-age=86400, enforce"
Note: Only add the enforce directive after verifying your certificate is in CT logs.
add_header Expect-CT "max-age=86400, enforce" always;
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.
curl -I https://yourdomain.com should include the expect-ct header. Verify your cert appears in crt.sh.
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.
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.
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;
Run openssl s_client -connect yourdomain.com:443 -status and look for OCSP Response Status: successful in the output.
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.
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Use HTTP Headers by WebFactory or Solid Security (formerly iThemes Security) to add HSTS without server access.
Run curl -I https://yourdomain.com and look for strict-transport-security in the response headers.
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.
Add to your server config:Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted-cdn.com;
HTTP Headers by WebFactory or WP Content Security Policy help you build and apply a policy without touching server config.
Once report-only shows no violations, switch to enforcement: Content-Security-Policy. Add nonces for inline scripts if needed.
Check the Network tab in browser DevTools — the response headers should include content-security-policy.
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.
Header always set X-Frame-Options "SAMEORIGIN"
add_header X-Frame-Options "SAMEORIGIN" always;
Add to the top of your .htaccess:<IfModule mod_headers.c>
Header set X-Frame-Options "SAMEORIGIN"
</IfModule>
Check response headers via browser DevTools or curl -I https://yourdomain.com.
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.
Apache: Header always set X-Content-Type-Options "nosniff"
Nginx: add_header X-Content-Type-Options "nosniff" always;
Confirm x-content-type-options: nosniff appears in your response headers.
Without a Referrer-Policy, your full URL (including query parameters with sensitive data) may leak to external sites when users click links.
Referrer-Policy: strict-origin-when-cross-origin
This is the recommended modern default — sends origin only on cross-site requests.
Confirm referrer-policy is present in response headers.
Without a Permissions-Policy header, scripts on your page can freely request access to the camera, microphone, geolocation, and other powerful browser APIs.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Only allow features your site actually uses.
Check for permissions-policy in response headers.
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.
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.
Header always set Cross-Origin-Opener-Policy "same-origin"
In your server block:add_header Cross-Origin-Opener-Policy "same-origin" always;
curl -I https://yourdomain.com should include cross-origin-opener-policy: same-origin.
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.
Cross-Origin-Resource-Policy: same-site
Use same-origin for stricter control, or cross-origin only for public CDN assets that are intentionally shared.
Header always set Cross-Origin-Resource-Policy "same-site"
add_header Cross-Origin-Resource-Policy "same-site" always;
curl -I https://yourdomain.com should include cross-origin-resource-policy: same-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.
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_header Cross-Origin-Embedder-Policy "require-corp" always;
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.
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.
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.
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_header X-XSS-Protection "1; mode=block" always;
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.
curl -I https://yourdomain.com should include x-xss-protection: 1; mode=block.
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.
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>
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;
});
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.
Log into your site, then run curl -I https://yourdomain.com/wp-admin/. The response should include cache-control: no-store.
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.
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 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);
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.
View page source and check that all external <script> and <link> tags have integrity="sha384-..." and crossorigin="anonymous" attributes.
Attackers can detect WordPress from your HTML source and target known platform CVEs. Reducing fingerprinting limits automated attack surface.
Add to functions.php:remove_action('wp_head', 'wp_generator');
Plugins like WP Hide & Security Enhancer or WPS Hide Login change the default paths bots scan for.
Add to functions.php:remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');
View your page source (Ctrl+U) and search for "wp-content" — ideally paths are obfuscated or removed.
Your exact WordPress version is visible in the HTML source. Attackers cross-reference this with CVE databases to instantly identify exploitable vulnerabilities.
Add to functions.php:remove_action('wp_head', 'wp_generator');
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); });
The RSS feed also exposes the version. Add: add_filter('the_generator', '__return_empty_string');
View source and search for your version number — it should not appear anywhere.
The readme.html file is publicly accessible and prominently discloses your WordPress version number.
Add to your .htaccess:<Files readme.html>
Order Allow,Deny
Deny from all
</Files>
Delete /readme.html from your WordPress root. It is not needed for site operation. Also delete readme.txt and license.txt.
Visit https://yourdomain.com/readme.html — you should get a 403 or 404.
Visiting /?author=1 redirects to a URL containing the admin username. Attackers use these usernames for brute-force and credential-stuffing attacks.
Add to .htaccess (before WordPress block):RewriteCond %{QUERY_STRING} ^author=\d
RewriteRule ^ - [F]
Solid Security (iThemes) and Wordfence both have user enumeration blocking built-in under Brute Force Protection settings.
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; });
Visit https://yourdomain.com/?author=1 — the URL should NOT redirect to a path containing a username.
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.
<Files xmlrpc.php>
Order Deny,Allow
Deny from all
</Files>
add_filter('xmlrpc_enabled', '__return_false');
Use the Disable XML-RPC plugin which blocks external requests while still allowing Jetpack to function internally.
Visit https://yourdomain.com/xmlrpc.php — you should get a 403 Forbidden.
Your WordPress login is at the default /wp-login.php URL. Bots target this URL with automated brute-force attacks around the clock.
Install WPS Hide Login and set a custom slug like /my-secret-door under Settings → WPS Hide Login.
Wordfence or Solid Security limit login attempts and add CAPTCHA challenges. This is more robust than obscurity alone.
In .htaccess:<Files wp-login.php>
Order Deny,Allow
Deny from all
Allow from YOUR.OFFICE.IP
</Files>
Attempt to visit https://yourdomain.com/wp-login.php — it should return 404 or redirect.
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.
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; });
Both plugins include REST API user enumeration blocking in their settings under the API section.
Visit https://yourdomain.com/wp-json/wp/v2/users as a logged-out user — it should return empty or a 401/403.
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.
Add to your .htaccess:<Files license.txt>
Order Allow,Deny
Deny from all
</Files>
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.
Visit https://yourdomain.com/license.txt — you should get a 403 or 404.
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.
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.
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.
Under Users → Profile, set Display name publicly as to your full name or pen name — never your login username.
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.
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.
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.
Add to functions.php:add_filter('xmlrpc_methods', function($methods) {
unset($methods['pingback.ping']);
unset($methods['pingback.extensions.getPingbacks']);
return $methods;
});
Add to functions.php:add_filter('wp_headers', function($headers) {
unset($headers['X-Pingback']);
return $headers;
});
View page source and search for "pingback" — the X-Pingback link should not appear in the HTML head.
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.
Go to Settings → General and uncheck Anyone can register. Set the New User Default Role to Subscriber even if you do need registration.
Install WPForms or Simple Cloudflare Turnstile to add CAPTCHA to your registration form. This blocks automated bot registrations.
Use WP-Members or User Registration plugin to require email confirmation before accounts become active. This prevents fake/spam registrations.
Visit https://yourdomain.com/wp-login.php?action=register — it should show "User registration is currently not allowed" if you have disabled it.
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.
Add to functions.php:remove_action('wp_head', 'wlwmanifest_link');
Add alongside:remove_action('wp_head', 'rsd_link');
Both are legacy blogging client discovery endpoints with no value on modern WordPress sites.
View page source and confirm no wlwmanifest or rsd_link links appear in the <head> section.
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.
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');
});
Install Heartbeat Control by WP Rocket (free) for a GUI to configure per-page heartbeat frequency or disable it entirely without code.
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.
Open browser DevTools → Network and filter for "admin-ajax". Confirm requests are infrequent (60-second intervals) or absent on non-editor pages.
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.
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);
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.
Enable WP_DEBUG only on staging/development environments. Most hosting control panels (cPanel, RunCloud, Kinsta) let you create staging clones in one click.
Visit your live site and check: no PHP warnings, notices, or errors should appear. Use curl https://yourdomain.com | grep -i "notice:\|warning:\|fatal".
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.
Go to Dashboard → Updates and click "Update to [latest version]". WordPress will download and install the update automatically. Always back up first.
In wp-config.php, add:define('WP_AUTO_UPDATE_CORE', true);
This enables automatic security and minor release updates.
Via SSH: wp core update && wp core update-db
Before any update, create a full backup with UpdraftPlus or your host's backup tool. This gives you a rollback point if anything breaks.
Check Dashboard → About WordPress — version should match latest at WordPress releases.
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.
Add this to wp-config.php:define('DISABLE_WP_CRON', true);
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
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>
After setting DISABLE_WP_CRON, confirm cron jobs still run by checking scheduled events with wp cron event list.
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.
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;
});
To only block user enumeration:add_filter('rest_endpoints', function($endpoints) {
if (isset($endpoints['/wp/v2/users'])) unset($endpoints['/wp/v2/users']);
return $endpoints;
});
Disable REST API or Solid Security offer settings to restrict REST API access without writing code.
curl https://yourdomain.com/wp-json/wp/v2/users should return a 401 or 403 after restriction.
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.
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.
Solid Security is another excellent free option with 2FA, brute-force protection, and security hardening. Available from the WordPress plugin repository.
Sucuri offers both a free WordPress plugin (auditing, blacklist monitoring) and a premium cloud WAF that filters traffic before it reaches your server.
After installation, check the security plugin's dashboard for its status. Ensure the firewall is active and a scan shows no threats.
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.
Add to wp-config.php:define('DISALLOW_FILE_EDIT', true);
This removes the Appearance → Theme Editor and Plugins → Plugin Editor menu items entirely.
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.
Log in as admin and check Appearance — the "Theme File Editor" option should be absent from the menu.
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.
Go to Plugins → Installed Plugins. Click Update Available to filter, then select all and use Bulk Actions → Update. Always backup first.
For each plugin, click Enable auto-updates in the plugin list. Or add to functions.php:add_filter('auto_update_plugin', '__return_true');
Via SSH: wp plugin update --all
Check for updates: wp plugin status
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.
Go to Dashboard → Updates — it should show "Everything is up to date!"
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.
Go to Appearance → Themes, hover over your active theme and click Update if available. Back up your child theme customizations first.
Keep only your active theme and one default WordPress theme (for emergency recovery). Delete all others via Appearance → Themes → Theme Details → Delete.
Never directly modify a parent theme — create a child theme instead. This allows parent theme updates without overwriting your customizations.
Dashboard → Updates should show no theme updates pending.
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.
In wp-config.php before installing, change:$table_prefix = 'wp_';
to something like:$table_prefix = 'sp7x_'; (use a random alphanumeric string).
Use Solid Security (iThemes) → Tools → Change Database Table Prefix. Always take a full database backup before this operation.
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.
Check wp-config.php — $table_prefix should not be wp_. Confirm the site still loads correctly after the change.
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.
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.
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.
Rotating salt keys invalidates all existing sessions immediately. This is a useful emergency response when you suspect a session has been compromised.
Open wp-config.php and confirm all 8 keys/salts are set to long unique random strings — not "put your unique phrase here".
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.
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).
Wordfence includes a reCAPTCHA option for the login form under Wordfence → Login Security. Uses Google reCAPTCHA v3 (invisible).
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.
Visit your login page (or custom login URL) and confirm a CAPTCHA or bot challenge is present.
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.
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.
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.
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.
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.
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.
Create a .htaccess file inside /wp-content/uploads/ with:<Files *.php>
Order Deny,Allow
Deny from all
</Files>
Add to your server block:location ~* /wp-content/uploads/.*\.php$ {
deny all;
return 403;
}
Extend the block to cover: .php, .php5, .phtml, .phar, .cgi, .pl, .py. Malicious uploads often use alternate extensions.
Upload a test PHP file to media library, then try to access it directly via URL — it should return 403 Forbidden.
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.
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.
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.
Sucuri's WAF proxies all traffic through their infrastructure, filtering attacks before they reach your server. Includes virtual patching for known WordPress vulnerabilities.
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.
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.
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.
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.
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.
Go to Settings → UpdraftPlus Backups and confirm the last backup date is within your scheduled interval. Check that remote storage is configured.
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.
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;
});
Install a GraphQL security layer that disables introspection in production while allowing it in development environments based on authentication status or environment variable.
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.
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.
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.
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.
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".
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;
}
Attempt 10 rapid login requests to your login page — you should receive a block response (403) or be challenged before the 10th attempt.
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.
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
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.
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.
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.
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.
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]
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.
Visit https://yourdomain.com/wp-includes/class-wp.php — it should return 403 Forbidden.
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.
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.
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.
Use the Two Factor plugin to require 2FA for all administrator accounts. See the 2FA guide in this knowledge base.
Users → All Users → Filter by Administrator. You should see only the minimum necessary admin accounts, all with recognizable names and active email addresses.
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.
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.
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.
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.
Check Dashboard → Updates regularly. Consider installing Easy Updates Manager plugin for granular control over what updates automatically.
No cookie consent banner or privacy notice was detected. Under GDPR (EU), CCPA (California), and similar laws, sites that use tracking cookies (analytics, advertising, social media pixels) must obtain informed user consent before setting non-essential cookies. Non-compliance can result in significant fines.
Install Complianz – GDPR/CCPA Cookie Consent (free) or CookieYes. Both auto-scan your site for cookies, generate a privacy policy, and present a compliant consent banner.
At minimum, distinguish between: Necessary (always on), Analytics (opt-in), Marketing/Advertising (opt-in), and Social Media (opt-in). Block tracking scripts until consent is given.
WordPress provides a privacy policy template under Settings → Privacy. Customize it to list all cookies your site uses, what data you collect, and how it's processed.
Open your site in an incognito/private browser window — a cookie consent banner should appear before any non-essential cookies are set.
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.
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).
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.
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.
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.
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.
Edit /etc/apache2/apache2.conf (or httpd.conf) and add:ServerTokens Prod
ServerSignature Off
In nginx.conf inside the http {} block:server_tokens off;
If you use Cloudflare, enable Security Level and use Transform Rules to remove the Server header entirely.
Run curl -I https://yourdomain.com — the Server header should say Apache or nginx without a version number, or be absent entirely.
The X-Powered-By header reveals your backend language and framework version (e.g., PHP/8.1.2), making targeted attacks easier.
Find and edit php.ini (run php --ini to find it) and set:expose_php = Off
Then restart your web server.
Header unset X-Powered-By
Requires mod_headers enabled.
proxy_hide_header X-Powered-By;
fastcgi_hide_header X-Powered-By;
curl -I https://yourdomain.com should no longer include x-powered-by.
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.
Review each Disallow: line. Remove entries that reveal sensitive infrastructure paths. Protecting a path from crawlers is not security — anyone can read robots.txt.
Instead of hiding paths in robots.txt, protect the real resources: require authentication, add IP restrictions, or block access in .htaccess / Nginx config.
A common minimal approach: User-agent: *\nDisallow: /wp-admin/ — this is already public knowledge for WordPress sites and avoids revealing custom paths.
Review https://yourdomain.com/robots.txt and confirm no sensitive internal paths are listed.
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.
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
Add a location block:location /server-status {
deny all;
return 404;
}
The /server-info endpoint reveals loaded Apache modules, configuration directives, and compile-time settings. Apply the same IP restriction or disable mod_info.
Visit https://yourdomain.com/server-status from an external IP — it should return 403 or 404.
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.
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.
Add to your .htaccess:php_flag display_errors Off
php_flag log_errors On
In wp-config.php:define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', 0);
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.
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.
Add to .htaccess:<FilesMatch "^(CHANGELOG|CHANGES|HISTORY|RELEASE_NOTES|UPGRADING)(\.md|\.txt|\.rst)?$">
Deny from all
</FilesMatch>
location ~* ^/(CHANGELOG|CHANGES|HISTORY|RELEASE_NOTES)(\.md|\.txt|\.rst)?$ {
deny all;
return 404;
}
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/
Visit https://yourdomain.com/CHANGELOG.md — it should return 403 or 404.
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.
Log into Cloudflare Dashboard → select your domain → Caching → Configuration → Development Mode → toggle Off.
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.
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.
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).
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.
The plugin auto-corrects HTTP URLs in content, options, and the database on activation.
Use Better Search Replace plugin to replace all http://yourdomain.com with https://yourdomain.com in the database.
As a fallback, add to your CSP: Content-Security-Policy: upgrade-insecure-requests — this tells browsers to upgrade HTTP resource requests to HTTPS automatically.
Open browser DevTools → Console. There should be no "Mixed Content" warnings after loading your page.
Cookies without Secure, HttpOnly, or SameSite flags are vulnerable to theft via XSS or network interception, which can lead to session hijacking.
Add to wp-config.php:define('COOKIE_SECURE', true);
define('COOKIE_HTTPONLY', true);
In php.ini:session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = "Strict"
For all cookies, add via Apache:Header edit Set-Cookie ^(.*)$ $1;Secure;HttpOnly;SameSite=Strict
In browser DevTools → Application → Cookies, check that your session cookie shows ✔ Secure, ✔ HttpOnly, and has a SameSite value.
An open directory listing allows anyone to browse your server file structure and download files — including backups, config files, and private data.
Add to .htaccess (or main server config):Options -Indexes
In your server block: autoindex off;
Place a blank index.php file (just <?php // silence) inside directories that should not be browsable.
Visit the directory URL that was flagged — it should return 403 Forbidden, not a file listing.
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.
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
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;
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;
});
});
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.
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.
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);
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>';
});
Solid Security includes an "Away Mode" and idle session logout feature under Security → Settings → User Groups → Logout Idle Users.
Log in without "Remember Me", wait beyond the expected expiry period, and confirm the session has ended.
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.
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
Search your codebase for $_GET['password'], $_GET['token'], $_GET['key']. Replace with $_POST or header-based retrieval.
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.
Review your site's access logs for patterns like ?password=, ?token=, or ?key= appearing in logged URLs.
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.
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]
location ~* \.(jpg|jpeg|png|gif|webp)$ {
valid_referers none blocked yourdomain.com www.yourdomain.com;
if ($invalid_referer) { return 403; }
}
In Cloudflare Dashboard, go to Scrape Shield → Hotlink Protection and toggle it on. This is the easiest one-click solution.
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.
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.
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);
For login redirects, use relative paths like /dashboard rather than full URLs. Strip any http:// or https:// prefix before redirecting.
WordPress uses redirect_to in login forms. Wordfence and Solid Security validate this parameter. Ensure you're running a modern version of these plugins.
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.
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.
Add to your .htaccess:<Files ".env">
Order allow,deny
Deny from all
</Files>
Add to your server block:location ~ /\.env {
deny all;
return 404;
}
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.
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.
Run curl -I https://yourdomain.com/.env — the response should be 403 Forbidden or 404 Not Found, not 200 OK.
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.
Add to your .htaccess:RedirectMatch 404 /\.git
Or:<DirectoryMatch "^\.git">
Deny from all
</DirectoryMatch>
location ~ /\.git {
deny all;
return 404;
}
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.
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.
curl -I https://yourdomain.com/.git/config must return 403 or 404.
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.
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*.
<FilesMatch "wp-config\.php\.(bak|old|copy|orig|tmp|~)$">
Deny from all
</FilesMatch>
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.
curl -I https://yourdomain.com/wp-config.php.bak must return 403 or 404.
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.
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.
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.
Add to Apache .htaccess:<FilesMatch "\.(sql|bak|backup|dump|db)$">
Order allow,deny
Deny from all
</FilesMatch>
Assume the dump has been downloaded. Change your database password and update wp-config.php. Force-reset all admin accounts in WordPress.
curl -I https://yourdomain.com/backup.sql should return 403 or 404.
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.
Remove phpinfo.php and info.php from your webroot: rm /var/www/html/phpinfo.php
Nginx: location ~ /(phpinfo|info)\.php { deny all; return 404; }
Apache: <Files "phpinfo.php"> Deny from all </Files>
curl -I https://yourdomain.com/phpinfo.php must return 403 or 404.
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.
Add to your .htaccess:<Files "debug.log">
Order allow,deny
Deny from all
</Files>
In wp-config.php, ensure:define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);
If you need debug logging, move it outside the webroot:define('WP_DEBUG_LOG', '/var/log/wordpress-debug.log');
curl -I https://yourdomain.com/wp-content/debug.log should return 403 or 404.
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.
<Files "install.php">
Order allow,deny
Deny from all
</Files>
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>
curl -I https://yourdomain.com/wp-admin/install.php should return 403.
.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.
Via FTP or SSH, delete .DS_Store from any public web directory. Check recursively: find /var/www/html -name ".DS_Store" -type f -delete
<Files ".DS_Store">
Order allow,deny
Deny from all
</Files>
location ~ /\.DS_Store {
deny all;
return 404;
}
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.
curl -I https://yourdomain.com/.DS_Store should return 403 or 404.
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.
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.
<Files "composer.json">
Order allow,deny
Deny from all
</Files>
<Files "composer.lock">
Order allow,deny
Deny from all
</Files>
location ~* composer\.(json|lock)$ {
deny all;
return 404;
}
curl -I https://yourdomain.com/composer.json should return 403 or 404.
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.
<Files "package.json">
Order allow,deny
Deny from all
</Files>
<Files "package-lock.json">
Order allow,deny
Deny from all
</Files>
location ~* package(-lock)?\.json$ {
deny all;
return 404;
}
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.
curl -I https://yourdomain.com/package.json should return 403 or 404.
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.
In your Nginx config:location ~ /\. {
deny all;
return 404;
}
This blocks all dot-files and dot-directories from being served.
Apache natively blocks .htaccess by default. Confirm via AllowOverride None in the global config and no override for <Files ".ht*">.
Check curl -I https://yourdomain.com/.htaccess, /.htpasswd, and /.env — all should return 403 or 404.
curl -I https://yourdomain.com/.htaccess should return 403 or 404, never 200 with text content.
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.
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
<FilesMatch "(\.(swp|swo|bak|orig|tmp)|~)$">
Deny from all
</FilesMatch>
location ~* (\.(swp|swo|bak|orig|tmp)|~)$ {
deny all;
return 404;
}
In Vim, add to ~/.vimrc: set directory=$HOME/.vim/swaps// to store swap files in a non-webroot directory.
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.
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.
Delete adminer.php, phpmyadmin/, or similar directories from your public webroot. Database management should never be done from a public URL on production.
In Apache .htaccess:<Files "adminer.php">
Order Deny,Allow
Deny from all
Allow from YOUR.OFFICE.IP
</Files>
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.
Visit https://yourdomain.com/adminer.php and https://yourdomain.com/phpmyadmin/ — both should return 403 or 404.
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.
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.
<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.
location ~* \.(log)$ {
deny all;
return 404;
}
curl -I https://yourdomain.com/error.log should return 403 or 404.
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.
Install Wordfence Security (free) or run a scan via Sucuri SiteCheck (sucuri.net/website-scanner). These tools identify modified files and malware injections.
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.
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.
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.
After cleanup, re-scan with Sucuri SiteCheck and Wordfence. Confirm no obfuscated code appears in page source via browser DevTools → View Source.
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.
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
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.
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.
View page source and check all <script src= tags. Every domain should be recognizable (your domain, major CDNs, or known services).
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.
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.
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.
After cleaning, submit your site for a Manual Actions review in Google Search Console. This lifts any spam penalties and restores rankings.
Use curl https://yourdomain.com | grep -i "viagra\|casino\|payday" — should return no matches after cleanup.
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.
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.
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.
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.
Run curl https://yourdomain.com | grep -i "http-equiv" — no meta refresh tag pointing to an external domain should appear.
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.
Enable maintenance mode or add deny from all to your .htaccess to protect visitors while you clean.
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.
If you have a pre-infection backup (via UpdraftPlus, BlogVault, or your host), restore it. Verify the backup is clean before restoring.
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.
After cleanup, run Sucuri SiteCheck and Wordfence full scan. Both must show "no malware found".
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.
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.
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"
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.
Add to .htaccess inside /wp-content/uploads/:<Files *.php>
Deny from all
</Files>
This prevents uploaded PHP webshells from executing.
After cleanup, run Wordfence and Sucuri full scans. Confirm no PHP files exist in uploads: find /var/www/html/wp-content/uploads -name "*.php".
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.
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%';"
A cryptominer implies full site compromise. Follow the known malware signature remediation steps: isolate, scan, restore from clean backup, rotate all credentials, update everything.
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.
View page source and search for "coinhive", "cryptonight", "miner". Run Sucuri SiteCheck — it specifically checks for mining scripts.
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.
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"
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).
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.
View page source and search for "key", "secret", "token", "password". Verify no secret credentials appear in rendered HTML or linked JavaScript files.
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.
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).
Add a second record:yourdomain.com. CAA 0 issuewild "letsencrypt.org"
Run dig CAA yourdomain.com or use MXToolbox CAA Lookup.
Without DNSSEC, attackers can forge DNS responses and redirect your visitors to malicious servers without anyone noticing (DNS poisoning / cache poisoning).
Most registrars (Cloudflare, Namecheap, GoDaddy) offer DNSSEC in their DNS settings panel. Enable it with one click — they handle the key generation automatically.
If you use a separate DNS provider (e.g., Route 53, Cloudflare), enable DNSSEC there first, then add the DS record at your registrar.
Use Verisign DNSSEC Analyzer or run dig +dnssec yourdomain.com and look for the AD flag.
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.
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.
In your DNS manager, add a new record:
Type: AAAA, Name: @ (and www), Value: your server's IPv6 address.
Use ipv6-test.com to confirm your site is reachable over IPv6 after DNS propagation.
Run dig AAAA yourdomain.com — you should see an IPv6 address in the answer section.
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.
In BIND (named.conf):zone "yourdomain.com" {
type master;
allow-transfer { none; };
};
For Cloudflare or Route53 users, zone transfers are disabled by default.
Cloudflare, AWS Route53, and Google Cloud DNS all block AXFR by default and offer DDoS-resilient anycast DNS as a bonus.
Test: dig @ns1.yourdomain.com yourdomain.com AXFR
You should see Transfer failed or REFUSED, not a list of records.
dig @ns1.yourdomain.com yourdomain.com AXFR should return Transfer failed.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Run dig -x YOUR.SERVER.IP.HERE — the answer should return a valid hostname pointing back to your domain.
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.
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.
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.
SPF records support max 10 DNS lookups. If you use many services, flatten the record using SPF Surveyor.
Run dig TXT yourdomain.com and look for the SPF record, or use MXToolbox SPF Check.
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.
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.
After reviewing reports for 2–4 weeks, upgrade to p=quarantine (spam folder), then p=reject (block completely).
Services like DMARC Analyzer, Postmark, or Dmarcian parse your reports and show you which sending sources fail.
Run dig TXT _dmarc.yourdomain.com or check with MXToolbox DMARC.
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.
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.
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.
Send an email from your domain to a Gmail address and click the ⋮ menu → Show original — look for DKIM: PASS.
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.
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 a TXT record: _mta-sts.yourdomain.com → v=STSv1; id=20240101000000Z
Update the id whenever you change the policy.
Visit https://mta-sts.yourdomain.com/.well-known/mta-sts.txt — it should return your policy file.
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.
BIMI requires an enforced DMARC policy. Set p=quarantine or p=reject in your _dmarc TXT record before proceeding.
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 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.
Run dig TXT default._bimi.yourdomain.com to confirm the record exists. Use BIMI Generator to validate your setup.
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.
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.
Services like Mailhardener or Dmarcian accept and parse TLS-RPT reports alongside DMARC reports, giving you a unified dashboard.
Run dig TXT _smtp._tls.yourdomain.com — you should see the TLSRPTv1 record in the answer.
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.
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.
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.
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.
Log out and log back in as an admin. You should be prompted for a 2FA code before gaining access to the dashboard.
FTP transmits your username, password, and all file contents in plaintext. Any network observer can capture your credentials. Replace FTP with SFTP immediately.
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).
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.
Run nmap -p 21 yourdomain.com — the result should show filtered or closed.
Telnet sends everything including passwords in plaintext and has no security. Disable it immediately and use SSH instead.
sudo systemctl stop telnet
sudo systemctl disable telnet
sudo apt remove telnetd -y
sudo ufw deny 23
nmap -p 23 yourdomain.com must show closed or filtered.
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.
Edit /etc/mysql/mysql.conf.d/mysqld.cnf and set:bind-address = 127.0.0.1
Restart: sudo systemctl restart mysql
sudo ufw deny 3306
In your cloud firewall/security group, remove any inbound rule allowing port 3306 from 0.0.0.0/0.
nmap -p 3306 yourdomain.com should show filtered.
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.
Edit /etc/redis/redis.conf:bind 127.0.0.1
Restart: sudo systemctl restart redis
In redis.conf:requirepass YOUR_STRONG_PASSWORD_HERE
sudo ufw deny 6379
redis-cli -h yourdomain.com ping should time out or be refused.
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.
Edit /etc/ssh/sshd_config:PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Restart: sudo systemctl restart sshd
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.
sudo apt install fail2ban — automatically bans IPs after repeated failed login attempts.
Run sudo fail2ban-client status sshd to confirm protection is active.
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.
Edit /etc/mongod.conf:net:
bindIp: 127.0.0.1
Restart: sudo systemctl restart mongod
In mongod.conf:security:
authorization: enabled
Create a dedicated user with minimum required permissions.
sudo ufw deny 27017
Or use your cloud provider's security group / firewall rules to block 27017 from all external IPs.
Run nmap -p 27017 yourdomain.com from an external machine — the port should show filtered or closed.
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.
Edit postgresql.conf (find it with sudo -u postgres psql -c "SHOW config_file;"):listen_addresses = 'localhost'
Restart: sudo systemctl restart postgresql
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.
sudo ufw deny 5432 — apply immediately regardless of app config, as a second layer.
nmap -p 5432 yourdomain.com from an external host should show filtered or closed.
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.
SSH into the server and run: sudo ss -tlnp | grep 8080 or sudo lsof -i :8080 to identify the process.
If it's a dev server, stop it: sudo systemctl stop <service-name> and disable it: sudo systemctl disable <service-name>
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.
nmap -p 8080 yourdomain.com from outside should show filtered.
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.
Run: sudo ss -tlnp | grep 8443 to see what process is listening.
If it's a control panel (Plesk, cPanel, Webmin), ensure it uses a valid TLS certificate, has 2FA enabled, and IP allowlisting configured.
sudo ufw allow from YOUR.OFFICE.IP to any port 8443sudo ufw deny 8443
This limits the panel to known office IPs only.
Confirm the service at port 8443 is expected and intentionally accessible. If not, block it at the firewall.
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.
Run sudo ss -tlnp | grep 25. If you are not running a mail server, identify and stop the service.
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.
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
nmap -p 25 yourdomain.com from an external host should show filtered or closed.
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).
Edit /etc/memcached.conf and set:-l 127.0.0.1
Restart: sudo systemctl restart memcached
sudo ufw deny 11211
Also block UDP 11211 specifically since DDoS amplification uses UDP:sudo ufw deny proto udp to any port 11211
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.
nmap -p 11211 yourdomain.com should show filtered. Test UDP: nmap -sU -p 11211 yourdomain.com.
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.
Edit /etc/elasticsearch/elasticsearch.yml:network.host: 127.0.0.1
Restart: sudo systemctl restart elasticsearch
In elasticsearch.yml:xpack.security.enabled: true
Then run: bin/elasticsearch-setup-passwords auto to generate passwords for all built-in users.
sudo ufw deny 9200
Also block 9300 (Elasticsearch cluster transport port):sudo ufw deny 9300
curl http://yourdomain.com:9200 from an external host should time out or be refused.
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.
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.
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.
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.
curl http://yourdomain.com:2375/version from an external host must fail. nmap -p 2375,2376 yourdomain.com should show both ports filtered.
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.
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.
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.
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.
nmap -p 3389 yourdomain.com from an external network should show filtered or closed.
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.
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;
});
Remove the header that advertises pingback capability:add_filter('wp_headers', function($headers) {
unset($headers['X-Pingback']);
return $headers;
});
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.
Test using: curl -d '<?xml version="1.0"?><methodCall><methodName>pingback.ping</methodName></methodCall>' https://yourdomain.com/xmlrpc.php — should return faultCode or 403.