Posts Tagged #WordPress

WordPress is a popular open-source content management system (CMS) that can be used to build a wide variety of different websites.

Posted by Arnon Erba in Meta on .

When I left Blogger for WordPress in 2016, I made a deliberate choice to leave some features behind. Part of the allure of WordPress was the opportunity to explore PHP and to take as much ownership as I could over the inner workings of my blog. Blogger, by nature, never provided the level of customizability I was searching for, but the the chance to write my own custom WordPress theme was too appealing to pass up. Suddenly, I had the ability to customize, rewrite, and (more often than not) break almost everything that governed how my content was displayed on the Web.

Taking control did not come without tradeoffs. As previously mentioned, I never implemented comments, and until my massive update in May of this year I had some minor SEO problems like poorly apportionedĀ <h1> tags. However, I had a platform that allowed me to experiment with web technologies on a much larger scale than before.

That was fine, because that was the point. I don’t blog for ad revenue, or to become famous (though I have my doubts that running a technology blog is really the quickest path to Internet fame). Regardless, in the spirit of constant improvement, I’ve been slowly adding features to my blog as I feel it needs them.

Once I finished some SEO work and some performance optimization, the next task was to fix the comments. I’ve implemented the generic WordPress comments template and enabled the discussion section for everything but my archived posts. In fact, I can tell it’s working, because I’ve already blocked dozens of Russian spam comments. Please enjoy.

Updated Posted by Arnon Erba in Meta on .

It’s been almost exactly two years since I left Blogger for WordPress, and I haven’t looked back since. This month, I’m excited to announce the first major update to Arnon on Technology since its 2016 reboot.

Ancient History

I’ve been writing about technology on and off since 2012, when I set up a simple blog on Blogger. After discovering that my chosen URL of had been taken, I settled on the name “Technology by AE” and immediately published a rant about the new tab bar layout in Safari 6.

In the years that followed, my blog went through several major iterations. I posted extensively throughout 2013 and 2014 and refined my original electric green Blogger theme. In fact, the header image for this post is the original background image from 2012. However, by 2015, I had become tired of Blogger’s limitations, and was struggling to find time to write and post articles.

Finally, I manually migrated all my old posts to a fresh WordPress blog. Google had recently released its new design language, Material Design, and I built a Material Design-inspired WordPress theme to accompany the new site. “Technology by AE” became simply “Blog – Arnon Erba”, and it entered life as a sub-page under my own personal website.

Over the next few years, I worked on refining the content I posted. I launched the Server Logs Explained series and wrote some exhaustive posts about specific computer problems I had faced and how I fixed them. I rolled out HTTPS and worked on optimizing the layout and design for speed and accessibility. Still, the site felt like it was still hiding in the shadows, not quite used to its full potential.

What’s New

The first major change was to choose a real name for my blog, something I should have done from the start. In keeping with some of my favorite industry blogs — Joel on Software, Krebs on Security, and of course SwiftOnSecurity — I am excited to finally call this site Arnon on Technology. No more esoteric or ambiguous page titles.

I’ve made a lot of other changes, some obvious and some less so. I fixed some issues with the <h1>-<h6> header tags, making the site more accessible and more easily discoverable by search engines. I’ve continued to make some backend optimizations, and have made the site fully accessible via IPv6. But mainly, I’ve substantially changed the design once again.

Ever since rolling out my original Material Design theme (dark grey text on a white background), I’ve always wanted to try a light-on-dark variant. This theme is the realization of that dream, and is reminiscent of my original eye-searing Blogger theme. Personally, I like the new theme, and it comes with a wide variety of subtle improvements, especially for mobile devices. Hey, if Ars Technica can still offer a dark theme…

Looking Forward

My goal has always been to post useful or entertaining content — ideally both. I hope I’ve achieved that goal, at least occasionally, and I have a reasonably large stock of drafts stored up that I’m hoping to finish and post in the coming months. At any rate, it seems like two years is the average life of my site-wide redesigns, so we’ll see what the future holds.

Updated Posted by Arnon Erba in Server Logs Explained on .

Since WordPress is such a popular website platform (a.k.a. Content Management System, or CMS), there’s a multitude of different ways to misconfigure it. Consequently, there’s also someone out on the Internet who will attempt to exploit every possible misconfiguration in your WordPress installation. This time on Server Logs Explained we’ll be looking at one such attempt, albeit a fairly basic one.

The Logs

WordPress sites get random requests all the time for pages that should normally be off-limits to the outside world, which brings us to today’s log excerpt: - - [19/Jun/2016:07:37:57 -0400] "GET /wp-admin/post-new.php HTTP/1.1" 302 5 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0"

This hardly borders on an attempted exploit, since it’s just a plain HTTP GET request for the post-new.php page. However, if you’re still getting familiar with WordPress’s file structure, it can be a good learning experience to examine some of the unexpected requests your server gets.

Call and Response

First, let’s see what happened next, since the server returned a 302 Found (moved temporarily) response code. A 302 response generally provides the client with an alternate URL it should try instead: - - [19/Jun/2016:07:37:57 -0400] "GET /wp-login.php? HTTP/1.1" 200 1307 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0"

In plain English, the server politely asked the client to try wp-login.php instead, and the client followed the redirect.

This is exactly what’s supposed to happen, because post-new.php is the admin page that allows a site author to create a new post. Like the other admin pages in the /wp-admin/ directory, it should only be accessible to users who are logged in and have the permission to create new posts. If an unauthenticated user tries to access an admin page, WordPress is supposed to redirect them to the login page so that they can authenticate (or not).

Bonus Section: A Little Information About Query Strings

The most interesting part of this exchange is the request to wp-login.php, so let’s examine that. Stripping away the extra information from the second log excerpt leaves us with the request the client made to wp-login.php:

GET /wp-login.php? HTTP/1.1

The first part, GET, is the HTTP method used (as opposed to POST, etc.). Check out Mozilla’s HTTP request methods documentation if you are curious about other methods defined in the HTTP standard. The last part, HTTP/1.1, is the version of HTTP that was used between the client and the server, so we can skip that as well.

The middle part is the request the client made to wp-login.php, and it includes an interesting query string. The query string begins with ? and looks like this:


Query strings are one way to allow data to be passed to a webpage. This is useful if your webpage is coded in a dynamic language, like PHP, and can accept input. This particular query string contains two pieces of data, separated by the & character:

  1. redirect_to, a string, and
  2. reauth, a value.

The data contained in the request is highlighted in the following example:


Let’s look at each piece of data individually:


The URL in redirect_to is where wp-login.php will send the client to after it has successfully authenticated. If you examine the WordPress source code, you can locate the section in the wp-login.php that handles the redirection logic.

If we clean up the URL by replacing the escaped characters with their human-readable counterparts, we can see what the full URL looks like:

In other words, after successfully authenticating, the client would have been redirected to post-new.php, which was where it was trying to go in the first place.


ReAuth is a special WordPress feature that forces the client to re-authenticate no matter what. Normally, when you sign into your WordPress site (and you have cookies enabled in your browser), you’ll stay logged in to WordPress for a certain amount of time. This is because WordPress is able to store cookies locally in your browser that indicate that you’ve already authenticated properly and shouldn’t need to log in again. However, as a safety method, if WordPress fails to detect a proper login cookie, it redirects the client to wp-login.php and forces it to re-authenticate and acquire new cookies by setting reauth=1.

Updated Posted by Arnon Erba in Server Logs Explained on .

A couple weeks ago, I covered what a WordPress brute-force attack looks like. However, you may have realized that trying an unlimited number of passwords is futile if you don’t know any valid usernames to guess passwords for. Fortunately for crackers, there’s a simple way to abuse the WordPress “pretty permalinks” feature to obtain valid usernames for a WordPress installation. Fortunately for us, there’s a simple way to block this with Nginx.

The Logs

Like a brute-force attack, a user enumeration attempt is usually pretty easy to spot. The logs usually start out like this: - - [23/Jun/2016:17:04:11 -0700] "GET /?author=1 HTTP/1.1" 302 154 "-" "-"

And then continue like this… - - [23/Jun/2016:17:04:12 -0700] "GET /?author=2 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:13 -0700] "GET /?author=3 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:15 -0700] "GET /?author=4 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:16 -0700] "GET /?author=5 HTTP/1.1" 302 154 "-" "-"

…until the cracker gives up. - - [23/Jun/2016:17:04:18 -0700] "GET /?author=6 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:19 -0700] "GET /?author=7 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:20 -0700] "GET /?author=8 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:22 -0700] "GET /?author=9 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:23 -0700] "GET /?author=10 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:25 -0700] "GET /?author=11 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:26 -0700] "GET /?author=12 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:28 -0700] "GET /?author=13 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:29 -0700] "GET /?author=14 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:30 -0700] "GET /?author=15 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:32 -0700] "GET /?author=16 HTTP/1.1" 302 154 "-" "-" - - [23/Jun/2016:17:04:33 -0700] "GET /?author=17 HTTP/1.1" 302 154 "-" "-"

Why This Works

By default, WordPress uses query strings as permalinks, such as:

This example permalink would display a post with the ID number “123”. The post ID is generated when a new post is created. Query strings make for “ugly” permalinks, however, so WordPress allows you to enable “pretty permalinks” using Apache mod_rewrite or a custom try_files directive in Nginx. With “pretty permalinks” enabled, WordPress performs an HTTP 301 redirect from the “ugly” permalink to the “pretty permalink” configured on the Settings>Permalinks screen.

WordPress doesn’t just have IDs for posts, though. Every WordPress user, or author, has a unique ID that maps to their archive page, which is a list of all the posts that they have created. “Ugly” author permalinks look like:

When pretty permalinks are enabled, the author archive page looks like:

This reveals the author’s WordPress username. A simple script can easily enumerate all the usernames on a WordPress site by trying ?author= with sequential numbers, as we saw in the log excerpts above.

Mitigating WordPress User Enumeration Attempts

We can block user enumeration on two levels: by redirecting the “ugly” permalinks, or by redirecting the /author/ pages entirely. Keep in mind that even if you disable the /author/ pages, your username can be discovered through other methods, and you should assume it is publicly available knowledge. However, we can make it difficult for the public to obtain that knowledge.

Disable query-string based user enumeration

A simple if statement works to disable user enumeration using query strings (or “ugly” permalinks). This is a “safe” if statement in Nginx (see the infamous If Is Evil page) since we are using it with a return statement.

if ($args ~ "^/?author=([0-9]*)") {
        return 302 $scheme://$server_name;

This code uses a simple regex, or regular expression, to match any URIs that end in /?author= plus a number. Here’s how it works:

$args is an Nginx variable for the query string
~ indicates that we want Nginx to perform a case-sensitive regex match using the regular expression inside the double quotation marks
^ (the carat) indicates the beginning of the path
/?author= is the fixed part of the path
([0-9]*) is a capturing group that matches any combination of numbers between 0 and 9

The return statement then redirects any URIs that fit the pattern.

Disable WordPress author pages entirely

We can add a simple location block to disable the author pages entirely. This solution is a bit redundant, because you would have to already know the author’s username to access their /author/ archive page, but this is useful if you don’t want author archive pages on your blog for some reason.

Note: this solution, by itself, does not prevent user enumeration, because the intermediary step between the query string and the author archive page pretty permalink will not be hidden. In other words, the query string will redirect to the archive page, revealing the username, and then will redirect based on the code below.

location ~ ^/author/(.*)$ {
        return 302 $scheme://$server_name;

~ starts a case-sensitive regex match, like above
^ starts the path we want to match
/author/ indicates we want paths beginning with /author/ to be matched
(.*) is a capturing group that matches any character except newlines
$ marks the end of the path

If a URI is matched, it is redirected to the root server name using return, like above.

Updated Posted by Arnon Erba in Server Logs Explained on .

There are a couple different ways that crackers will try to get into your WordPress installation, and one of them is by using a plain old brute-force attack. This kind of attack requires nothing more than a freely available exploit toolkit, and is not difficult to detect in the server logs. In the first section of this post, I’m going to give an example of what a brute force attack looks like, and then to make things more interesting I’ll discuss some techniques used to mitigate them using Nginx.

The Logs

As you would guess, when one computer makes hundreds of requests for a resource in quick succession, it leaves some pretty serious traces in the server logs (these are real logs, but I removed the server name): - - [22/Jun/2016:19:18:58 -0700] "POST /wp-login.php HTTP/1.1" 200 3848 "" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; 125LA; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)" - - [22/Jun/2016:19:18:59 -0700] "POST /wp-login.php HTTP/1.1" 200 3848 "" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; 125LA; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)" - - [22/Jun/2016:19:18:59 -0700] "POST /wp-login.php HTTP/1.1" 429 0 "" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; 125LA; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)" - - [22/Jun/2016:19:19:00 -0700] "POST /wp-login.php HTTP/1.1" 429 0 "" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; 125LA; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)" - - [22/Jun/2016:19:19:00 -0700] "POST /wp-login.php HTTP/1.1" 429 0 "" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; 125LA; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)" - - [22/Jun/2016:19:19:00 -0700] "POST /wp-login.php HTTP/1.1" 429 0 "" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; 125LA; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)"

Here’s a couple things about these requests that make it obvious that this is a brute-force attack (other than the fact that they go on for about half an hour).

  1. The HTTP method is POST, which indicates data is being sent to the server (i.e. the actual password guesses).
  2. The resource requested is /wp-login.php, which is the default WordPress login page and should rarely be requested, even by legitimate users.

If you look more closely, however, you’ll see something interesting: the HTTP response code that the server returns starts off as 200 OK, but quickly transitions to 429 Too Many Requests. This is one method of fending off brute force attacks with Nginx.

Mitigating WordPress Brute-Force Attacks

Fortunately, WordPress brute-force attacks are not that difficult to defend against without the use of plugins or additional software. We can:

  1. Restrict access to the login page to a curated list of IP addresses,
  2. Explicitly block the IP addresses of known brute-force offenders with Nginx or with a firewall,
  3. Password-protect the login page using HTTP Basic Authentication,
  4. Or, my personal favorite: set up rate-limiting with Nginx to cut down on how many requests attackers can make in a certain period of time.

Restrict Access to Certain IP Addresses

Arguably, the best way to mitigate brute-force attacks is to restrict access to the WordPress login page to only known good IP addresses. Here’s what that looks like with Nginx:

location = /wp-login.php {
    deny all;
    # add your PHP fastcgi config here

This location block explicitly targets the /wp-login.php page and only allows clients using the IP addresses and to access it. All other requests will be met with a 403 Forbidden error message. Keep in mind you will need to add your PHP fastcgi config to this location block as well so that Nginx knows to pass legitimate requests back to PHP. If you’re not familiar with how to do this, either consult the Nginx docs regarding PHP or keep an eye out for a newer post.

This method ensures that attackers will never get access to the login page, but is difficult to maintain if legitimate WordPress users do not have static IP addresses.

Deny Access from Certain IP Addresses

Another solution is to explicitly block brute-force offenders. You can block certain IP addresses from accessing the login page with:

location = /wp-login.php {
    # add your PHP fastcgi config here

If you are familiar with configuring firewalls, you can use firewall commands to block the IP address from accessing anything on your server at all.

While blocking specific IP addresses can be useful, I don’t recommend using this as your only line of defense. For one, any IP address used in a brute force attack is almost certainly a VPN, proxy, or bot IP address. By blocking these, you risk denying access to legitimate users, even if that risk is slight. The main concern is that maintaining a list of IP addresses is tedious and unwieldy and is not a good long-term solution. That’s not to say this approach is useless, however, as you may want to use it in tandem with another one.

With that in mind, the next possible solution is adding a second layer of protection to the WordPress login page with HTTP Basic Auth.

Restrict Access Using HTTP Basic Auth

There are two steps to using HTTP Basic Auth with WordPress and Nginx.

  1. Create the password file
  2. Configure Nginx

I am going to skip the first step in this post, as there are many good existing guides on using openssl or apache2-utils to create a password file (see here or here).

The second step, configuring Nginx, is fairly simple. Just add two lines to your wp-login location block:

location = /wp-login.php {
    auth_basic "Restricted Content";
    auth_basic_user_file /path/to/.password_file;
    # add your PHP fastcgi config here

You can change “Restricted Content” to any phrase you want, as it will be the message that end-users see when they attempt to access the login page. Make sure you enter the correct path to your password file you created as well.

While password-protecting the login page is a valid solution, it has the potential to overly complicate the login process for legitimate users.

Using Rate Limiting in Nginx

Nginx has some great documentation on how to implement rate limiting, but I am going to provide an example of how to optimize it for WordPress. Setting up rate limiting in Nginx is simple, and only requires two components:

  1. We must define a zone in the main nginx.conf file.
  2. We must implement that zone in the WordPress login location block.

To define the zone, we use limit_req_zone and, optionally, limit_req_status. These directives go inside the http block of the main nginx.conf configuration file.

http {
     limit_req_zone $binary_remote_addr zone=wordpress:10m rate=15r/m;
     limit_req_status 429;

The above snippet defines a 10 MB zone named “wordpress” that allows a maximum of 15 requests per minute from any one IP address. The limit_req_zone requires a variable, or key. In this case, the key is $binary_remote_addr, or the IP address of the client. Nginx will use a maximum of 10 MB of memory to store the keys, and if a key exceeds the maximum number of allowed requests, Nginx will terminate the connection and return the status code defined in limit_req_status. The default code is 503 Service Unavailable, but I prefer the more specific 429 Too Many Requests response. Keep in mind that Nginx will display a blank page to the client for non-standard HTTP codes if you have not set a custom error page using the error_page directive.

You can name the zone anything you want (it is named “wordpress” in the example above) and you can also define any rate limit you feel is appropriate. I found that allowing a maximum of 15 requests per minute is restrictive enough to hamper a brute-force attack but is permissive enough not to interfere with end-users who legitimately mistyped their passwords.

To actually use the zone, we must implement it by adding this code to the WordPress login location block:

location = /wp-login.php {
    limit_req zone=wordpress;
    # add your PHP fastcgi config here

This tells Nginx to limit requests to the /wp-login.php page using the parameters specified in the zone we defined above. Make sure you replace “wordpress” with whatever you named your zone in the previous step. Restart or reload Nginx and rapidly refresh your login page to test if the new brute-force protection is working. If you refresh faster than the rate you defined in limit_req_zone, the server will return the status code defined in limit_req_status.

Obligatory note: if you’ve read other guides on how to set up rate limiting with Nginx, you may have seen other syntaxes used, such as limit_req zone=one burst=1 nodelay. The burst and nodelay options are more complex and allow you to control what happens to excess requests. They are not necessary in this context, since we want any excess brute-force attempts to be immediately rejected, but I would highly encourage you to read the documentation for them here.


This is by no means an exhaustive list for preventing brute-force attacks. Other solutions exist in the form of WordPress plugins or intrusion prevention systems such as Fail2ban. However, a lot can be accomplished by correctly configuring Nginx, and the less WordPress plugins you have installed, the better.

Updated Posted by Arnon Erba in Meta on .

(Editor’s note: Less than three months after writing this post, I got fed up with Blogger and finally jumped ship. Now ALL the links are broken, so I have to start from scratch again from the SEO side of things, but I finally have a functioning blog that is completely mine.)

It seems like every six months my blog reinvents itself, at least layout-wise. I think it’s because nothing I do ever looks quite finished in my eyes. At any rate, ever since I discovered Material Design Lite, I’ve been meaning to build my own custom Blogger template using the framework. Now that I’ve hacked together something that could be considered a material design template for Blogger, I can sum up my experience so far in one sentence: I wish I was using WordPress.

At any rate, I’ve made a lot of changes, but there’s still more to go. All the old posts need to be re-formatted (again), and the comments disappeared, partially because I broke them. Expect to see more changes – and maybe some actual posts again – soon.