Completely Hiding The Wordpress Version Is Hard

We develop and maintain a couple of WordPress sites. There are a number of things we will do as standard to secure each site. One of these is running WPScan. It checks for all kinds of things but one that caught my interest is how it determines which version of WP is running. I went down a bit of a rabbit hole to see if I could defeat it and wanted to share what I found.

Meta Data

There are two places where the version is intentionally displayed:

  • In the HTML: <meta name="generator" content="WordPress X.X.X" />
  • In the XML RSS feed: <generator>https://wordpress.org/?v=X.X.X</generator>

The simplest way to defeat this is to add a MU plugin:

<?php

remove_action('wp_head', 'wp_generator');
add_filter('the_generator', fn(): string => '');

Query Strings

Some of the core JS and CSS assets URLs include the version in a query string ?ver=X.X.X. This is presumably for cache busting.

This can be defeated by creating another MU plugin:

<?php

function removeWPVersionQueryString($src) {
    if (strpos($src, 'ver=' . get_bloginfo( 'version'))) {
        $src = remove_query_arg('ver', $src);
    }

    return $src;
}

add_filter('style_loader_src', 'removeWPVersionQueryString');
add_filter('script_loader_src', 'removeWPVersionQueryString');

Headers

There are two scripts used in the WP admin which leak the version number in an etag header. You don't have to be logged in to access these files:

  • /wp-admin/load-scripts.php
  • /wp-admin/load-styles.php

This can be defeated by using the following Apache config:

<LocationMatch "/wp-admin/load-(scripts|styles)\.php">
    Header unset Etag
</LocationMatch>

Fingerprinting

This one is trickier. WPScan downloads some files from a predefined list and calculates their hashes. It then cross references the hashes to WP releases. Clever!

This can be defeated using Apache's ext_filter module:

ExtFilterDefine proxiedcontentfilter mode=output cmd="/bin/sed $a/*nYDfgwQADg69A*/"

<LocationMatch ".(css|js)$">
    SetOutputFilter proxiedcontentfilter
</LocationMatch>

This will add the string /*nYDfgwQADg69A*/ to the end of all JS and CSS files. There is nothing special about the string it is just a comment with a random string in it. All it does is change the hash of the file.

Version Spoofing

There is a simpler but more invasive method of defeating all the above. Again another MU plugin:

<?php
$wp_version = explode('.', $wp_version)[0];

 This simply truncates the version number to show only the major version. So 6.0.2 becomes 6. This massively reduces the fidelity of the version number. The only issue I've found with this is that some plugins will use this value to check compatibility and rely on the full version.

Just Because You Can Doesn't Mean You Should

There is a fair amount of discussion around the benefit of obscuring information like this in a security context. It is often referred to as security through obscurity. It shouldn't matter that an adversary can see you are running version X of a thing because it should always the latest patched version. I believe there is a limited amount of merit in fending off automated drive-by style attacks but that is it.

That doesn't mean there isn't value in finding out that you can! I didn't know Apache had a ext_filter module. It's another tool in my developer tool belt which might prove useful for something in the future.

Popular Reads

Subscribe

Keep up to date

Please provide your email address
Please provide your name
Please provide your name
No thanks