One of the Ten Most Popular WordPress Plugins Isn’t Needed and Introduces a Vulnerability on Some Websites Using It
One of the issues we have run into with the web security industry that seems to be rather telling as to its poor state, is the number of people that think that it isn’t a problem that companies are misleading, lying, and outright scamming people, but it is a problem to point out that companies are doing those things. There have been plenty instances where people have told us that we shouldn’t be pointing out that companies are engaged in those types of practices. Keeping quiet about those things though is harmful as can be seen in what we recently found when looking at one of the ten most popular WordPress plugins, which has over 2+ million active installations according to wordpress.org.
When it comes to the WordPress security, one of the most repeated claims is that there are lots of brute force attacks against WordPress admin passwords. We have seen many security companies making that claim and then claiming that their plugin or service is the solution. The problem with this is that based on security companies own data, brute force attacks are not happening. For a type of attack that is happening, dictionary attacks, WordPress does a good job of helping to protect against them. That might be one reason why security companies are misleading people, since if they told the truth, it wouldn’t be reason for people to use their plugins and services.
On the basis of those types of false claims the plugin Limit Login Attempts has gotten to the tenth spot in terms of the most popular plugins (one spot ahead of Wordfence Security). Despite not having been updated in six years and only listed as being tested up to WordPress 3.3.2, it continues to be added to more websites. The other day we decided to take a quick look at the plugin and we quickly found that the plugin has a vulnerability, though one that is only exploitable if you changed one of the plugin’s default settings. We also found in that situation that the plugin’s ability to limit login attempts can be easily bypassed and that two other similar, though much less popular plugins, that we happened to run across while looking at this, suffer from the same issue without having to have changed their settings.
One question this is raises is how there could be what appears to be a previously undisclosed vulnerability in such a popular plugin that has been there for so many years. Beyond the fact that most plugins have not had a review of their security, despite what seems to be a popular belief that they would have, this vulnerability is a bit hidden. We were able to spot it quickly through a combination of looking over the plugin’s code, having seen others disclose related vulnerabilities, having found related vulnerabilities ourselves, and having a robust ability to test out for possible vulnerabilities (acquired due to our testing out each claimed vulnerability before adding them to the data set of our service). That is a combination that doesn’t appear to exist at any other company (despite frequent claims that they have unmatched capabilities).
The plugin’s function limit_login_get_address(), which is located in the file /limit-login-attempts.php, is supposed to get the IP address that a login attempt is coming from. It looks like this as of the latest version, 1.7.1:
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | /* Get correct remote address */ function limit_login_get_address($type_name = '') { $type = $type_name; if (empty($type)) { $type = limit_login_option('client_type'); } if (isset($_SERVER[$type])) { return $_SERVER[$type]; } /* * Not found. Did we get proxy type from option? * If so, try to fall back to direct address. */ if ( empty($type_name) && $type == LIMIT_LOGIN_PROXY_ADDR && isset($_SERVER[LIMIT_LOGIN_DIRECT_ADDR])) { /* * NOTE: Even though we fall back to direct address -- meaning you * can get a mostly working plugin when set to PROXY mode while in * fact directly connected to Internet it is not safe! * * Client can itself send HTTP_X_FORWARDED_FOR header fooling us * regarding which IP should be banned. */ return $_SERVER[LIMIT_LOGIN_DIRECT_ADDR]; } return ''; } |
Unlike other plugins where related issues have been discovered, just looking at that piece of code you wouldn’t know where the IP address could possibly come from. By default the address would come from the variable $_SERVER[‘REMOTE_ADDR’], which returns the “The IP address from which the user is viewing the current page.” By changing the plugin’s “Site connection” setting to “From behind a reversy proxy” it will instead use $_SERVER[‘HTTP_X_FORWARDED_FOR’]. If the plugin isn’t being used behind a reverse proxy that sends the related HTTP header X-Forwarded-For, that becomes a problem because the value of that can be specified by the requester.
That leads to a vulnerability because by default, failed login attempts are logged, without the IP address being validated or sanitized, which occurs in the function limit_login_notify_log():
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 | /* Logging of lockout (if configured) */ function limit_login_notify_log($user) { $log = $option = get_option('limit_login_logged'); if (!is_array($log)) { $log = array(); } $ip = limit_login_get_address(); /* can be written much simpler, if you do not mind php warnings */ if (isset($log[$ip])) { if (isset($log[$ip][$user])) { $log[$ip][$user]++; } else { $log[$ip][$user] = 1; } } else { $log[$ip] = array($user => 1); } if ($option === false) { add_option('limit_login_logged', $log, '', 'no'); /* no autoload */ } else { update_option('limit_login_logged', $log); } } |
That value is then output without being escaped, which would permit persistent cross-site scripting (XSS) to occur, if say someone set the value of the HTTP header X-Forwarded-For to malicious JavaScript code. The lack of escaping occurs in the function limit_login_show_log():
/* Show log on admin page */ function limit_login_show_log($log) { if (!is_array($log) || count($log) == 0) { return; } echo('<tr><th scope="col">' . _x("IP", "Internet address", 'limit-login-attempts') . '</th><th scope="col">' . __('Tried to log in as', 'limit-login-attempts') . '</th></tr>'); foreach ($log as $ip => $arr) { echo('<tr><td class="limit-login-ip">' . $ip . '</td><td class="limit-login-max">'); $first = true; foreach($arr as $user => $count) { $count_desc = sprintf(_n('%d lockout', '%d lockouts', $count, 'limit-login-attempts'), $count); if (!$first) { echo(', ' . $user . ' (' . $count_desc . ')'); } else { echo($user . ' (' . $count_desc . ')'); } $first = false; } echo('</td></tr>'); } }
The value of $log passed in there comes directly from where it was stored by the function limit_login_notify_log():
1085 | $log = get_option('limit_login_logged'); |
Since the HTTP header X-Forwarded-For is user specified (unless it is set by a proxy between the requester and the website), if someone was actually doing brute force attacks they could simply change what seems to be the IP address the request comes from for each request, so they would never hit login attempt limit. It should also be possible to lock someone else out if you knew what there IP address is, by simply sending their IP address as the value of X-Forwarded-For for repeated failed requests.
Part of what makes something like this stand out is that while this plugin is very popular, despite not being all that useful and insecure, the companion plugin for our service, which when used without our service will still warn about the usage of plugins that have vulnerabilities that look to be being exploited, so it provides real protection, has only 4,000+ active installs. That speaks to a much larger issue that despite the real threat that exploitable vulnerabilities in WordPress plugins pose they don’t get anywhere near the attention they should, due to things like the false claims from security companies about brute force attacks (which would make a lot of sense for them if as we have put forward, the security industry is truly the insecurity industry). It also doesn’t help that founder of WordPress believes that the only real security problem related to plugins is not keeping them up to date, which obviously isn’t the case when you have vulnerabilities being found in plugins that are no longer supported (his company is also involved in pushing the brute force attack narrative).
Proof of Concept
With the plugin’s “Site connection” setting to “From behind a reversy proxy”, attempt to log in to WordPress with the X-Forwarded-For HTTP header set to “<script>alert(document.cookie);</script>” until you are locked out. That header can be set in Chrome using the ModHeader extension.
Afterwards, when visiting the page /wp-admin/options-general.php?page=limit-login-attempts any available cookies will be shown in an alert box.