WordPress: the 9 holes our audit finds - and how to close them

4 min readWordPresssecurityaudit

Not a generic tips list - exactly what our free audit checks on every site. With code for every item.

Our free audit checks a site against more than sixty items, nine of them about security. Below are all nine, in the same order of severity the audit itself uses: critical first, then major, then minor but free.

Critical: found in a minute by a scanner

1. .env and .git are reachable from outside. An .env file holds database passwords and payment keys; .git holds the entire source history. Automated scanners try these paths daily on every site they encounter - not a question of whether they get found, only when.

<FilesMatch "^\.(env|git)">
    Require all denied
</FilesMatch>
RewriteRule ^\.git - [F,L]
On Apache/LiteSpeed - in .htaccess. Treat any keys that were already there as already leaked, and rotate them.

2. http does not redirect to https. Anyone arriving without encryption stays on an unprotected connection - on public Wi-Fi their password and form data travel in plain text. Google also treats the http and https versions as two separate sites.

The fix is a permanent 301 redirect at the server, not in PHP: loading all of WordPress just to redirect to itself is a wasted second on every wrong request.

Major: not critical, but an expensive gap

3. /wp-json/wp/v2/users serves administrator logins to anyone who asks. Half of breaking in is guessing the username; here there is nothing to guess, WordPress names it for you.

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

4. No HSTS. Without this header, the very first visit still goes over plain http even when the https redirect is configured - and that first request is the one that can be intercepted. The browser needs to know in advance that this host is https-only.

add_action( 'send_headers', function () {
    header( 'Strict-Transport-Security: max-age=15552000; includeSubDomains' );
} );
max-age in seconds - half a year here, the minimum that is actually worth setting.

5. The site can be framed by anyone. Without a frame restriction, another site can load yours in an invisible iframe and place its own buttons on top: the visitor thinks they are clicking your button and are clicking theirs. On an account page or an order form that is money, not an abstract risk.

add_action( 'send_headers', function () {
    header( 'X-Frame-Options: SAMEORIGIN' );
} );

6. Session cookies without Secure and HttpOnly. Without HttpOnly, any third-party script on the page can read the cookie; without Secure, it travels unencrypted. If it is the administrator's session cookie, stealing it means logging in as them.

There is a trap specific to WordPress behind a proxy or CDN: core decides whether to set the Secure flag through is_ssl(), which checks $_SERVER['HTTPS']. If TLS terminates at the CDN rather than on the server itself, WordPress never sees that and silently skips Secure, even though the visitor is genuinely on https.

if ( ! empty( $_SERVER['HTTP_X_FORWARDED_PROTO'] )
    && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) {
    $_SERVER['HTTPS'] = 'on';
}
Right at the top of wp-config.php - before anything else gets a chance to call is_ssl().

Minor: but free

7. readme.html and xmlrpc.php are open. readme.html names the exact WordPress version, so a scan only has to match it against known vulnerabilities. xmlrpc.php allows hundreds of password attempts in a single request.

The same filter we gave in the speed article to save a request works here to close a hole - disable xmlrpc unless something depends on it.

add_filter( 'xmlrpc_enabled', '__return_false' );

8. The server announces its version. A header like X-Powered-By: PHP/7.4 tells an attacker which set of known vulnerabilities to try first. Not a hole in itself - a free hint.

9. No Content-Security-Policy. A CSP is the list of places a page is allowed to load scripts from. Without one, a foreign script that gets in through a vulnerable plugin or a compromised ad slot runs with the same rights as your own code.

CSP is the one item of the nine you cannot just switch on: you first have to learn which domains the site genuinely uses. Start with Content-Security-Policy-Report-Only, watch the browser console for a week, and only then turn it on for real.

9security checks in our free audit - all nine from this post

None of these nine show up on the homepage - they do not affect how the site looks, which is exactly why nobody notices until it is too late. That is why the audit checks them automatically instead of hoping someone remembers on their own.