Prevent User Enumeration in WordPress

We recently had to deal with a hacking attempt against a client WordPress site that had a few interesting aspects. The fix involved additional .htaccess rules to block user enumeration.

We experienced multiple failed login attempts against WordPress. This wasn’t particularly worrying – the originating IP address was automatically blocked by our Fail2Ban setup after three unsuccessful attempts. The attacker (probably a script) then switched IP address and repeated the process, trigerring a further ban and repeating the cycle. The attack lasted for approximately 10 minutes and triggered more than 50 bans.

The attack focused on usernames that were very close to (but not actually the same as) actual usernames on the site. It looked like a partially-successful user-enumeration attempt made up the initial phase of the attack. Puzzlingly, only some usernames had been enumerated.

User Enumeration

User Enumeration is when would-be attackers collect usernames by interacting with your app. Unfortunately, by default WordPress makes this process easy. Entering http://example.com/?author=1 in the browser will trigger display of all articles authored by the user with an ID of ‘1’ – along with their registered username. This provides would-be attackers with a toe-hold – they can attempt to log in to valid usernames rather than having to guess.

Our usual setup involves user-enumeration prevention measures – so it was surprising to see (almost valid) usernames cropping up in the log.

It turns out that our user-enumeration prevention relied on ‘redirect_canonical’ WordPress filter. This filter is triggered if you navigate to http://example.com/?author=1 – in this case, it performs a redirect to the Author archives for the author with an ID of 1.

The problem: If a registered user on the site has not authored any articles, the redirect will not take place. The user does not have an archive, the redirect doesn’t take place, and the user-enumeration can proceed.

In our case, the enumerated users had a custom membership role rather than an author role – so they will never have an archive page. In our context, these are pretty low risk users, with very few permissions on the site. Nevertheless, it’s a pain having to check when these attacks occur, and it places unecessary load on the server.

We verified the partially successful enumeration attempt by doing some penetration testing using WPScan – this turned up the exact “usernames” that were tried during the hack attempt.

The solution involved extra .htaccess rules to prevent user-enumeration. We also added some extra rules to block login attempts using the enumerated (incorrect) usernames – just in case the attacker is logging them for future usage.

.htaccess Rule to Prevent User enumeration


RewriteEngine On
%{REQUEST_URI} !^/wp-admin [NC]
RewriteCond %{QUERY_STRING} author=\d
RewriteRule (.*) $1? [L,R=301]

Explanation

Line One

Turn on rewriting functionality – the Apache mod_rewrite module must be installed on the server. This module rewrites requested URLs on the fly by means of a rule-based rewriting engine. The rewrite engine is based on a Perl Compatible Regular Expressions(PCRE) parser.

Line 2

Apply a rewrite condition such that the rule will be ignored if the REQUEST_URI begins with /wp-admin.

REQUEST_URI

The path component of the requested URI, such as “/index.html”. This notably excludes the query string which is available as its own variable named QUERY_STRING. — Apache mod_rewrite Docs

REQUEST_URI in simple terms is the bit after your domain.

The author=\d string that we’ll use to match the user enumeration attempt is used legitimately to display author posts in back end – so the rewrite rule should not apply if the request takes place in the WordPress admin area.

Line 3

Specify the rewrite condition – the target query string must include 'author=\d', where \d means a single digit.

This means that http://example.com/?dummy&author=1 will trigger the rewrite, as will http://example.com/?dummy&author=100 – provided we’re not in the admin area, as specified by the previous condition.

Note that the rule doesn’t specify that the ‘author’ variable is at the start of the query string (e.g. ^/?author=([0-9]*) – a query string that starts with /?author= followed by any number of digits).

Line 4

The rewrite rule: replace the entire path (.*) with itself $1 but with an empty query string ?.

Make this the last rule and specify that it is a permanent redirect [L,R=301].

TLDR: .htaccess Rules

Add these rules to .htaccess to prevent all malicious user-enumeration attempts. Such attempts will redirect to the site home page:



RewriteEngine On
RewriteCond %{REQUEST_URI} !^/wp-admin [NC]
RewriteCond %{QUERY_STRING} author=\d
RewriteRule (.*) $1? [L,R=301]

Note that preventing user-enumeration is only one component of an effective security policy.

References

Parse YAML in PHP Using Symfony YAML

Convert data in YAML format into a PHP array.

I do a lot of work in WordPress. I also build a lot of static websites – both for rapid design in-the-browser and as a low-cost small-business website solution. I mainly use the excellent Jekyll static site generator.

Jekyll uses YAML for config and data files (it can also use CSV format, but that’s another story). WordPress doesn’t use YAML.

I like YAML because it is very human friendly – the whole team (including non-developers) can easily build YAML config files in a way that you’re not going to see with formats like JSON or XML. This is an example of a YAML array used to create Javascript variables for use in a Google map:


# Map centre Latitude & Longitude
# ------------------------------------------------------------------------------
latitude: 52.7157856867271
longitude: -8.8741735070805

zoom: 15
#height: 548px

# Set custom colour variables
# ------------------------------------------------------------------------------
waterColour: "#398A8D"
landColour: "#dec7c7"
mainRoadColour: "#777777"
minorRoadColour: "#a9a9a9"

# A nested array
# ------------------------------------------------------------------------------
test:
- One
- Two
- Three

/_data/map.yml

In the context of Jekyll, you could use this as a data file – e.g./_data/map.yml – writing Javascript variables into `<head>` something like this:


<script>
var cwCentre = {
latitude:{{ site.data.map.latitude }},
longitude:{{ site.data.map.longitude }},
zoom:{{ page.map-zoom }},
mainMarker:"{{ site.baseurl}}/{{ site.data.map.mainMarker }}",
secondaryMarker:"{{ site.baseurl}}/{{ site.data.map.secondaryMarker }}",
waterColour:"{{ site.data.map.waterColour }}",
landColour:"{{ site.data.map.landColour }}",
mainRoadColour:"{{ site.data.map.mainRoadColour }}",
minorRoadColour:"{{ site.data.map.minorRoadColour }}",
title: "{{ site.data.map.title | escape }}",
description:'{{ map_description | markdownify | strip_newlines }}',
};
{% if "multi-centre" == page.map-type %}
var markers = [
{% for location in site.data.secondary-map-coords %}
[
'{{ location.name | escape }}',
{{ location.latitude }},
{{ location.longitude }},
'{{ location.description | escape }}'
]
{% unless forloop.last %},{% endunless %}
{% endfor %}
];
{% endif %}
</script>

YAML in WordPress

I recently needed to convert a Jekyll site to a WordPress theme. Moving the map config settings required parsing YAML data into a PHP array. Fortunately this can be achieved pretty easily thanks to the Symfony YAML component.

I’m a recent convert to Composer, and find it amazingly powerful. You can add the Symfony YAML component with a single composer command.

Add Symfony/YAML Using Composer


composer require symfony/yaml

When you run this, composer will add a new `symfony/yaml` directory under the project ‘vendor’ directory. It will also add the relevant namespace to the ‘autoload_psr4.php’ file, so that the new class will be autoloaded.

Using the YAML parser

To read the YAML contents of the config fields into a PHP array:


<?php
use Symfony\Component\Yaml\Parser;

$yaml = new Parser();

$value = $yaml->parse( file_get_contents( get_template_directory() . '/assets/map.yml' ) );

For the YAML content presented above, the following will be output:


$value = array (
'latitude' => 52.715785686727102,
'longitude' => -8.8741735070804992,
'zoom' => 15,
'waterColour' => '#398A8D',
'landColour' => '#dec7c7',
'mainRoadColour' => '#777777',
'minorRoadColour' => '#a9a9a9',
'test' => array (
0 => 'One',
1 => 'Two',
2 => 'Three',
),
);

This array can be passed to wp_localize_script() when enqueuing the map script.

The WordPress/PHP way would be to collect such data from a form on an admin page, storing the data in the wp_options table. However taking variables from YAML files can be a good way to quickly port settings, which might even be used as defaults. It might also be a good way to configure certain project settings.

Semantic Bootstrap Layout Classes with an Offset

Making CSS classes descriptive, or semantic, can help improve code maintainability by describing an elements purpose, rather than it’s presentational function.

People level this as a criticism against Bootstrap (and other CSS frameworks) – where column names are presentational rather than semantic. In other words, in a typical Bootstrap project the main content area might have a class name like .col-md-8, which is not semantic.

Fortunately, Bootstrap makes it pretty easy to define semantic classes that apply the built-in presentational logic, by use of the make-x-column()mixins.

SAGE – EXAMPLE OF SEMANTICALLY DEFINING CLASSES

The Sage WordPress starter theme (a great starting point for WordPress projects) defines a .main and a .sidebar class out of the box.

The column widths for these elements can then be set in a _variables.scssfile.

OFFSET SIDEBAR

I’ve modified the Sage definition of .main and .sidebar to add an offset to the sidebar, using the additional Bootstrap mixin make-sm-column-offset().

I find the offset really good for user experience – constraining the main content area generally makes for greater readability, and the extra whitespace between content and sidebar can reduce the sensation of clutter.



// Grid system

.main {

// No sidebar, `.main` is full width
@include make-sm-column($main-sm-columns);

// `.main` is contained by the `.sidebar-primary` parent class -

// this class is added to the <body> of pages that display the sidebar.

// If a sidebar displays, `.main` can take a reduced width.

.sidebar-primary & {

// `.main` is narrower by 2 columns, to give room for the offset
@include make-sm-column($main-sm-columns - $sidebar-sm-columns - 2 );

}

}

.sidebar {

// Set the width, then the offset, using Bootstrap mixins
@include make-sm-column($sidebar-sm-columns);
@include make-sm-column-offset(2);

}

RESOURCES

Checking Bash Scripts

I found this very useful resource that allows you check bash scripts online:

http://www.shellcheck.net/

I’ve been writing quite a few bash scripts lately – amongst other things, I find them useful for running automatic backups and scaffolding out projects from a Github repo starting point.

The tool helps with:

…typical beginner and intermediate level syntax errors and pitfalls where the shell just gives a cryptic error message or strange behavior, but it also reports on a few more advanced issues where corner cases can cause delayed failures.

  • http://www.shellcheck.net/about.html

It is certainly helping me to improve my bash scripting.

Linter Shellcheck in Atom

You can set up Shellcheck as an Atom package.

In Ubuntu:


# Install shellcheck on your system
sudo apt-get install shellcheck

# Install Base linter for Atom
apm install linter

# Install shellcheck
apm install linter-shellcheck

You’ll need to restart Atom.

Resources

 

Adding a .htaccess File to a Jekyll Site

How to have Jekyll build a .htaccess file into your project

If you serve your site with Apache, adding a .htaccess file to your document root allows fine control over access permissions.

Amongst other things, .htaccess rules can set:

  • In-browser caching
  • Access – you could allow/disallow access from certain IP addresses or ranges
  • Redirects
  • Rewrites

You can also prevent modification of code over 3G on some European providers (I’ve experienced UK providers in particular totally mangling site styles).

When setting up WordPress sites, I would typically lock down access to the entire admin area by IP address as a security measure. While this isn’t necessary for Jekyll sites (where there is no login), .htaccess rules can be a useful way of controlling how your site resources are cached. This has the potential to speed up site loading times.

.htaccess in Jekyll

Update: by default, Jekyll includes .htaccess files – so explicitly including your .htaccess file is unnecessary.

By default Jekyll excludes dotfiles – but you can easily override this behaviour.

In the project config.yml (or config_prod.yml if you have a production environment config file), add this line:


include: ['.htaccess']

Jekyll will now build the .htaccess file – which is much more convenient than editing the file on the server.

Create .htaccess file in the root of your Jekyll project.

When you build  the project, the .htaccess file will be included in the project root.

Sample .htaccess for Cache Control

The following .htaccess directive tells browsers to use cached content for the specified files. Just add the directive to the project .htaccess file:


# Set browser caching
# ------------------------------------------------------------------------------
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access 1 year"
ExpiresByType image/jpeg "access 1 year"
ExpiresByType image/gif "access 1 year"
ExpiresByType image/png "access 1 year"
ExpiresByType text/css "access 1 month"
ExpiresByType text/html "access 1 month"
ExpiresByType application/pdf "access 1 month"
ExpiresByType text/x-javascript "access 1 month"
ExpiresByType application/x-shockwave-flash "access 1 month"
ExpiresByType image/x-icon "access 1 year"
ExpiresDefault "access 1 month"
</IfModule>
# End caching block

WordPress Debugging

On-screen debugging output, writing errors to a log file, real time monitoring of the debug log

In general, error logging should be enabled in the development environment and disabled in production environments.

To enable WordPress error reporting to the browser, and to enable error logging to file, add the following lines to wp-config.php:

<?php
// Enable error reporting output to browser. Default value is false.
 define( 'WP_DEBUG', true );
// log errors to `/wp-content/debug.log`. Useful when debugging code that does not output to browser.
define('WP_DEBUG_LOG', true);

Continue reading » “WordPress Debugging”