Stop User Enumeration in WordPress
This tutorial explains how to block user-enumeration scans in WordPress. As explained in greater depth here, user enumeration happens when some malicious script scans a WordPress site for user data by requesting numerical user IDs. For example, requests for author=1
through some number, say, author=1000
, may reveal the usernames for all associated users. With a simple enumeration script, an attacker can scan your site and obtain a list of login names in a matter of seconds.
How it works
When scanning a site for user IDs, disclosure of user data happens in two ways. First, for permalink-enabled sites, requests for ?author=n
(where n
equals any integer) are redirected to the permalink version of the URL for that user, which by default includes the author’s login username. So for example, on a permalink-enabled site, the following URI requests:
http://example.com/?author=1
http://example.com/?author=2
http://example.com/?author=3
.
.
.
..automatically are redirected by WordPress to their “pretty permalink” counterparts:
http://example.com/author/admin-user/
http://example.com/author/wordpress-user/
http://example.com/author/some-other-user/
.
.
.
..of course, the actual usernames will vary depending on your site, but you get the idea.
The second reason that user enumeration works to reveal user data is that theme templates typically display the author name on author-archive pages, in post meta information, and possibly in other locations, depending on the theme.
For more details on user enumeration, check out my article on blocking user ID phishing requests over at htaccessbook.com.
Should you be worried?
If you are sure that all of your users are using strong passwords that are updated regularly, then there is nothing to worry about. This tutorial is aimed at sites with multiple authors who may not be “password savvy”. If an author is being lazy with their passwords, then user-enumeration could definitely put your site at risk. Equipped with a known username, a perpetrator quickly may gain access using a simple brute-force attack.
So to be safe, check out the following techniques to protect your site against user-enumeration and brute-force attacks. They take only a minute to implement, and will serve to harden your WordPress-powered site with additional layers of security.
Step 1: Disable the scans
The first thing we want to do is block the malicious enumeration scanning. This can be done in one of two ways:
- Add a code snippet to your theme’s
functions.php
file - Add a code snippet to your site’s root
.htaccess
file
Let’s check out each of these methods..
Block user-enumeration via functions.php
To block user-enumeration via functions.php, add the following code to your theme’s functions file:
// block WP enum scans
// https://m0n.co/enum
if (!is_admin()) {
// default URL format
if (preg_match('/author=([0-9]*)/i', $_SERVER['QUERY_STRING'])) die();
add_filter('redirect_canonical', 'shapeSpace_check_enum', 10, 2);
}
function shapeSpace_check_enum($redirect, $request) {
// permalink URL format
if (preg_match('/\?author=([0-9]*)(\/*)/i', $request)) die();
else return $redirect;
}
No editing is required for this to work, just copy/paste and done. Here’s how it works:
- Check if the request is for any page in the WP Admin Area
- Block the request if it’s for a query-string author archive
That’s the basic gist of it. Hit me up in the comments section for more specifics on what this code is doing, how it works, etc.
Block User Enumeration via .htaccess
If you would rather block requests at the server level, you can add the following slice of .htaccess to your site’s root .htaccess file:
# Block User ID Phishing Requests
<IfModule mod_rewrite.c>
RewriteCond %{QUERY_STRING} author=([0-9]*) [NC]
RewriteRule .* http://example.com/? [L,R=302]
</IfModule>
The only edit that’s required is the domain/URI, http://example.com/
, which you should change to match your own. For more information about this technique, check out my tutorial on blocking user-id phishing.
Step 2:
At this point, we’ve added a code snippet (in either functions or .htaccess) that will block those nasty user-enumeration scans. The second part of the equation is to make sure that your theme does not disclose the login username of any authors or users. Unfortunately, there is no quick, one-step solution for this step, as it requires careful examination of your theme. Here are some things to check:
- Author name displayed for each post
- Author name displayed for author-archive views
- Author name displayed anywhere else on the front-end
If your theme displays author names anywhere (as most themes do), there are few ways to prevent username disclosure:
- Change all user Display Names to anything other than the login name
- Make sure any author/user template tags are not displaying the login name
- Remove any template tags that display author/user login names
- Disable author archives entirely (if not needed)
Of course, this is a general guide that may not be applicable to every theme on the face of the planet (there’s only like a billion of them). But it should be enough to give you the idea and help you implement the best possible solution for your site.
15 responses to “Stop User Enumeration in WordPress”
Hey, Jeff, excellent tip as always. Do you plan to incorporate this in the next generation of your blacklist? I don’t always understand everything in those blacklists, and when upgrading to the eventual 7G, I’m curious if I’ll end up with redundant blocking at that point, which I suppose is a minor thing in the grand scheme of things. Haha.
Hey Rick, I like the idea but this sort of technique is best applied at the site level. Mostly because the 6G/7G are aimed at a broader user base, which includes non-WP sites and WP sites that aren’t using permalinks.
Update: Here is a tutorial that explains how to block user-ID phishing at the server level (via Apache/.htaccess). Gives better performance than the PHP method.
Hi Jeff,
thanks for the tips!
I disable the author archives with the option that the plugin Yoast SEO offers. If you do so, you get a 404 page which is not cached because of the query string. So I think the .htaccess rule is the most efficient way to block brute force attacks.
It becomes more complicated if you like to use the author archive pages. The author URL is using the value for “user_nicename” and this is by default exact the same as the login name. For just a few authors you can change the value via the database, but that is not a solution for sites with many authors or users.
Thanks for the feedback, Olaf!
Forgive me if i’very read it wrongly,but use of is_admin is a common coding slip up for checking user permissions (and I think worth highlighting – i’m guilty of the same error). From the codex:
“This Conditional Tag checks if the Dashboard or the administration panel is attempting to be displayed. It is a boolean function that will return true if the URL being accessed is in the admin section, or false for a front-end page.”
Not a slip-up in this case, as we are simply checking if the request is for any page in the WP Admin Area. Not trying to check if the user is logged in. I too have made that mistake in the past, but for this technique
is_admin
is correct. Thanks for the feedback.I think the issue is with this statement
> Check if the request is made by a user with admin-level capabilities
That appears to be describing the
is_admin()
function as checking the user cap, which is does not. The code is correct, but the description of what it is doing is not.Related, I’m wondering why you are doing a
preg_match
on$_SERVER['query_string']
instead of just checking to see ifisset($_GET['author'])
?Thanks Nick, you are correct about the description — I have updated the article accordingly.
For checking the
author
parameter, I wanted to keep the script as focused as possible to avoid any possibility of false positives. There may be plugins/themes that make use of theauthor
parameter on the frontend, so I didn’t want to interfere with anything. Just checking if the author is set may cause issues. That said, I should probably rephrase the snippet to usestripos
instead of the more costlypreg_match
.I got a lot of brute force attempt reported by security plugin I used. So, that thing above maybe very helpful to me. But, manually edited “things” are difficult to me, maybe you can mention a plugin that handle those work?
There are plenty of brute-force plugins available at the WP Plugin Directory would be your best bet.
Hi Jeff,
If I wanted to stop user enumeration in the functions php file, as you mention above, could I modify the code to redirect any ?author= attempts right back to the home page? Or does not that make sense? If so, how would I do that exactly? Please let me know. Thanks.
Yes you could use wp_safe_redirect() or similar to accomplish it.
Here is a better way to do it:
That is overkill, more code than needed. I’ve been using the method from the article for years now and it works great.
Hey Jeff.
Thanks for the snippet! I’ve been using it for some years now, along with other htaccess goodies you have posted!
Just today, I got notified by a customer that they couldn’t filter the posts by author in the admin panel.
Ha! I wasn’t even aware of this kind of filtering ,since most of our clients have 1-2 users max.
Anyways, just noting it: When on admin panel go to All Posts page and click in the name of an author.
The page will refresh with a url like:
https://your-site.com/wp-admin/edit.php?post_type=post&author=17
And list the posts by this author only.
The thing is that the htaccess rule also block’s this request.
I have modified the snipped with the following in order to allow requests coming from admin panel.
Thanks again for sharing your knowledge!