Spring Sale! Save 30% on all books w/ code: PLANET24
Web Dev + WordPress + Security

Stop User Enumeration in WordPress

[User Enumeration ] 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:

  1. Check if the request is for any page in the WP Admin Area
  2. 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.

FYI: You can also stop user enumeration with BBQ Pro Firewall »

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.

About the Author
Jeff Starr = Web Developer. Book Author. Secretly Important.
USP Pro: Unlimited front-end forms for user-submitted posts and more.

15 responses to “Stop User Enumeration in WordPress”

  1. Rick Beckman 2016/06/30 6:52 pmReply

    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.

    • Jeff Starr 2016/07/01 9:47 am Reply

      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.

  2. 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.

  3. 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.”

    • Jeff Starr 2016/07/28 8:29 am Reply

      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.

      • Nick the Geek 2016/09/26 12:20 pm

        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 if isset($_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 the author 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 use stripos instead of the more costly preg_match.

  4. Bojo Negoro 2016/08/13 7:38 pmReply

    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?

  5. Omar Abrahim 2016/09/28 9:02 amReply

    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.

  6. Here is a better way to do it:

    <IfModule mod_rewrite.c>
    	RewriteCond %{REQUEST_URI} !^/wp-admin [NC]
    	RewriteCond %{QUERY_STRING} ^author=\d+ [NC,OR]
    	RewriteCond %{QUERY_STRING} ^author=\{num [NC]
    	RewriteRule ^(.*)$ - [F,L]
    </IfModule>
    • Jeff Starr 2023/02/11 2:31 pm Reply

      That is overkill, more code than needed. I’ve been using the method from the article for years now and it works great.

      <IfModule mod_rewrite.c>
      	RewriteCond %{QUERY_STRING} author=([0-9]*) [NC]
      	RewriteRule .* /? [L,R=302]
      </IfModule>
  7. Apostolos G. 2023/09/26 5:52 amReply

    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.

    <IfModule mod_rewrite.c>
    	RewriteCond %{QUERY_STRING} author=([0-9]*) [NC]
            RewriteCond %{REQUEST_URI} !^/wp-admin/ [NC]
    	RewriteRule .* /? [L,R=302]
    </IfModule>

    Thanks again for sharing your knowledge!

Leave a reply

Name and email required. Email kept private. Basic markup allowed. Please wrap any small/single-line code snippets with <code> tags. Wrap any long/multi-line snippets with <pre><code> tags. For more info, check out the Comment Policy and Privacy Policy.

Subscribe to comments on this post

Welcome
Perishable Press is operated by Jeff Starr, a professional web developer and book author with two decades of experience. Here you will find posts about web development, WordPress, security, and more »
Digging Into WordPress: Take your WordPress skills to the next level.
Thoughts
I live right next door to the absolute loudest car in town. And the owner loves to drive it.
8G Firewall now out of beta testing, ready for use on production sites.
It's all about that ad revenue baby.
Note to self: encrypting 500 GB of data on my iMac takes around 8 hours.
Getting back into things after a bit of a break. Currently 7° F outside. Chillz.
2024 is going to make 2020 look like a vacation. Prepare accordingly.
First snow of the year :)
Newsletter
Get news, updates, deals & tips via email.
Email kept private. Easy unsubscribe anytime.