Skip to content
Uncategorized

WordPress Security Checklist: 9 Doors I Closed on My Own Site

A working WordPress security checklist, from the response headers nobody reads to the backup folder everybody forgets.

Mohammad Rifat Khan

Oct 2026

I run my own business website on WordPress, which means I am also the person who gets blamed if it is defaced, blacklisted or used to send spam. So this week I stopped assuming everything was fine and checked it properly.

No malware, no blacklist flags. But a free scan and five minutes of reading my own response headers showed several doors standing open, the kind almost every WordPress site leaves open because nothing bad has happened yet.

Here is the full list, in the order I worked through it. No security plugin, no monthly fee, and no code beyond a small must-use plugin and a server config file.

Start by reading your own headers

Before changing anything, look at what your site already tells the world. In a terminal:

curl -I https://yourdomain.com/

Every line that comes back is either information you are happy to publish or information you should not be publishing. Two worth looking for: server reveals the software and its version, and x-powered-by reveals your PHP version. If you can read them, so can a script that scans thousands of sites a day.

1. Force HTTPS and turn on HSTS

Every request should land on the secure version, and browsers should be told to remember that. Without HSTS, the very first request a visitor makes can still travel over plain HTTP, which is where interception happens.

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

2. Add the security headers

Five headers do most of the work, and none of them cost anything:

  • X-Content-Type-Options: nosniff stops a browser guessing that an uploaded file is a script
  • X-Frame-Options: SAMEORIGIN and frame-ancestors stop other sites framing yours to fake a login page
  • Referrer-Policy limits how much of your URL leaks to other sites
  • Permissions-Policy switches off camera, microphone, geolocation and payment access you never asked for

While you are there, remove the headers that only serve to advertise you: X-Powered-By and X-Pingback.

3. Turn XML-RPC off completely

xmlrpc.php is a WordPress file from an older era. It accepts remote publishing requests, which makes it the front door for two things: guessing thousands of passwords in a single request, and pingback abuse that turns your site into a weapon against someone else.

Nothing on my site used it. The block editor, my SEO plugin and my file backups all go through modern routes.

RewriteRule ^xmlrpc\.php$ - [F,L]

4. Close username discovery

This one is quietly important. On a default WordPress site, adding ?author=1 to the homepage redirects to /author/your-login-name/, which hands a brute force script the exact username it needs. The same list is available through the WordPress API.

Close both: send ?author= requests home, block author archives for visitors, and restrict the user list in the API to logged in sessions.

While you are there, check one thing in your profile: your public display name should not be the same as your login name. If it is, your username is already public in every byline on the site.

5. Stop PHP running inside your uploads folder

This is the one I would fix first if I could only fix one. Most WordPress compromises do not start with clever code, they start with a .php file dropped into a writable folder and then opened in a browser. Uploads, cache and backup folders never need to execute code.

Put this in a .htaccess file inside each of those folders:

<FilesMatch "\.(?i:php|phtml|php[0-9]?|phar)$">
	Require all denied
</FilesMatch>

6. Block directory listing and stray files

If directory listing is on, yourdomain.com/wp-content/uploads/ shows a full index of everything you have ever uploaded. Turn it off, then block the files that serve no purpose in public: readme.html, license.txt, wp-config-sample.php, database dumps, and any editor leftovers like .bak, .old or .swp.

7. Harden wp-config.php

Four lines, each closing a specific gap:

define( 'WP_HOME', 'https://yourdomain.com' );   // never trust the Host header
define( 'WP_SITEURL', 'https://yourdomain.com' );
define( 'FORCE_SSL_ADMIN', true );               // wp-admin over HTTPS only
define( 'DISALLOW_FILE_EDIT', true );            // no code editing from the dashboard

The first two matter more than they look. If your site builds its own URLs from whatever Host header arrives, an attacker can send a forged one and poison your cache, your links or your canonical tags.

Two more things while you are in that file: regenerate your security keys if the site has ever been moved or copied, and make sure database errors are never printed to visitors.

8. Put the rules where they survive

Server rules live in .htaccess, which is fragile. One cache plugin, one migration, one host control panel can rewrite that file and silently reopen every door you closed. So I put the same protections in two places: the server config, and a small must-use plugin in wp-content/mu-plugins/.

Must-use plugins load automatically, cannot be switched off by accident, and keep working if you change theme.

9. The door nobody thinks about

Here is the part that surprised me most, and it was not code.

When I stopped using a backup plugin, the plugin folder went away. The folder of database backups it had created did not. That is a full copy of a site’s database, every enquiry, every order, every hashed password, sitting on a web server, one bad rule away from being public.

If a backup plugin has ever run on your site, go and look at what it left behind. Then decide where your backups actually belong. Mine now live off the server entirely, and the host runs daily backups as a second copy.

How to check your own site in five minutes

curl -I https://yourdomain.com/
curl -I https://yourdomain.com/xmlrpc.php
curl -I https://yourdomain.com/wp-content/uploads/
curl -I "https://yourdomain.com/?author=1"
curl -I https://yourdomain.com/wp-json/wp/v2/users

The first should show your new headers. The other four should answer 403 or 404.

What I did not do, and why

I did not install a security plugin. Everything above is core WordPress, one config file and about thirty lines of PHP. Plugins are not the problem, but a plugin you do not understand is just another thing to maintain.

I also did not add a full Content Security Policy. My front end uses inline styles and scripts for the site design and for analytics, so a strict policy would have broken the layout and the mobile menu unless I rewrote how they load. I added the three directives that carry no risk (object-src 'none', base-uri 'self', frame-ancestors 'self') and left the rest for a day when it earns its keep.

The takeaway

Basic security is rarely about expensive tools. It is about closing the doors you forgot were open, then checking a month later that they stayed closed.

Two questions are worth five minutes of your time today: when did you last read your own response headers, and what is sitting in your wp-content folder that you stopped thinking about?

If you would rather have this done for you, this is exactly what my maintenance and integration service covers every month: updates, monitoring, security checks and a written summary of what changed. And if you want the plain version of what I do all day, it is on the services page.

Back to all posts