A WordPress user who was facing a small brute-force attack asked us for help. He was using a popular security plugin but, this time, his plugin was literally unable to protect the blog.
Having full root access to the server, I made a quick capture of the incoming TCP traffic on port 80 using the tcpdump
utility to see what was going on:
There was a brute-force attack on WordPress wp-login.php
page and the HTTP request included an X-Forwarded-For
header with an IP. But the most interesting part was that each consecutive POST request sent by the attacker had a different X-Forwarded-For IP
. In other words, those IPs were spoofed.
I reviewed the security plugin and found this piece of code:
function brute_get_ip() { if ( isset( $this->user_ip ) ) { return $this->user_ip; } $server_headers = array( 'HTTP_CLIENT_IP', 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR' ); if ( function_exists( 'filter_var' ) ) : foreach ( $server_headers as $key ) : if ( array_key_exists( $key, $_SERVER ) === true ) : foreach ( explode( ',', $_SERVER[ $key ] ) as $ip ) : $ip = trim( $ip ); // just to be safe //if the IP is private, return REMOTE_ADDR to help prevent spoofing if ( $ip == '127.0.0.1' || $ip == '::1' || $this->ip_is_private( $ip ) ) { $this->user_ip = $_SERVER[ 'REMOTE_ADDR' ]; return $_SERVER[ 'REMOTE_ADDR' ]; } if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) !== false ) : $this->user_ip = $ip; return $this->user_ip; endif; endforeach; endif; endforeach; else : // PHP filter extension isn't available
Instead of retrieving the user IP from the safe REMOTE_ADDR
variable, the plugin will first attempt to get it from 7 totally unreliable sources: HTTP_CLIENT_IP
, HTTP_CF_CONNECTING_IP
, HTTP_X_FORWARDED_FOR
, HTTP_X_FORWARDED
, HTTP_X_CLUSTER_CLIENT_IP
, HTTP_FORWARDED_FOR
and HTTP_FORWARDED
.
Because those headers are sent by the client (i.e., the attacker) to the server, they cannot be trusted. That is the reason why the security plugin was unable to help during the attack: it relied on the spoofed IPs.
Just the tip of the iceberg ?
I downloaded and tested the 15 most popular security plugins dealing with visitors IP, either recommended in the WordPress Codex (Hardening WordPress and Brute Force Attacks articles) articles, or found via the plugin directory search feature (searching for security
, attack
, firewall
keywords).
Out of the 15 plugins, 9 were vulnerable to IP spoofing.
I sorted them into 3 categories, based on the impact of the security flaw:
- Low: can display and/or log a spoofed IP.
- Medium: can blacklist and/or whitelist a spoofed IP.
- Critical: one or more security features of the plugin can be bypassed by using a spoofed IP.
All In One WP Security & Firewall:
Version: 3.8.3
Impact: critical
get_user_ip_address()
function inwp-security-utility-ip-address.php
:
phpwhois_getclientip()
function inwhois.ip.lib.php
:
BruteProtect:
Version: 2.2.6
Impact: critical
brute_get_ip()
function inbruteprotect.php
:
BulletProof Security:
Version: .50.8
Impact: medium
bpsPro_get_real_ip_address()
function indb-backup-security.php
:
Centrora Security (formerly OSE Firewall Security):
Version: 3.8.4
Impact: critical
getRealIP()
function inRemoteLogin.php
:
getRealIP()
function inipmanager.php
:
iThemes Security (formerly Better WP Security):
Version: 4.4.13
Impact: critical
get_ip()
function inclass-itsec-lib.php
:
Mute Screamer:
Version: 1.0.7
Impact: critical
ip_address()
function inUtils.php
:
Security-protection:
Version: 2.1
Impact: low
secprot_log()
function insecurity-protection.php
:
WordPress Simple Firewall:
Version: 3.5.3
Impact: critical
GetVisitorIpAddress()
function inicwp-data-processor.php
:
Wordfence:
Version: 5.2.4
Impact: critical
defaultGetIP()
function inwfUtils.php
:
phpwhois_getclientip()
function inwhois.ip.lib.php
:
The 6 plugins I tested that were not subject to IP spoofing:
- Apocalypse Meow
- Botnet Attack Blocker
- Login LockDown
- WP fail2ban
- WP Login Security 2
Preventing IP spoofing
If you are a PHP developer, preventing IP spoofing is easy:
$ip = $_SERVER['REMOTE_ADDR'];
Unless you are seriously looking for troubles, there is absolutely no alternative.
You can add an option to your plugin so that users can change it if their website is behind a reverse proxy, a load-balancer or a CDN, but it should remain optional only and should never be enabled by default.