Some of our clients love it when we log a lot of security related info in their applications. So on every authentication request we want to log the user’s IP and if we are denying access to some parts of the application we want to log this as well. So we have code like this all over our application:
authLogger.warn( "User #rc.username# from #getRealIp()# tried to do something dangerous" );
So can we detect the real IP of our users with high confidence?
The short answer: you can’t trace all the bad guys and people who want to stay anonymous, but for the majory of users you can get get some more info.
Long ago in the internet stone age, you just had to look at the cgi variable cgi.remote_addr
but internet is a lot more complicated nowadays both on the client side and the server side. Web access is not just simple access from the users browser to a server anymore. There might be Web Application Firewalls (WAF), content delivery networks (CDN), load balancers and more defense mechanisms involved on the route from browser to server. In one of our own setups for example we have cloudflare, a company firewall, haproxy loadbalancing and an nginx proxy in place before the user hits our lucee server. In a multi-proxy setup like this transparent proxies are adding an x-forwarded-for
header to the request. Each proxy appends the connecting ip to this header so it will look like:
X-Forwarded-For = remote-Ip,proxy-1-IP,proxy-2-IP,..etc
So in most cases you can determine the real IP by getting the first value in the list of x-forwarded-for addresses. If there is no x-forwarded-for header you can use the cgi.remote_addr
. A getRealIp()
function could look like this:
string function getRealIP(){
var headers = getHTTPRequestData( false ).headers;
if ( structKeyExists( headers, "X-Forwarded-For" ) )
{
return trim( listFirst( headers[ "X-Forwarded-For" ] ) );
}
return len( cgi.remote_addr ) ? trim( listFirst( cgi.remote_addr ) ) : "127.0.0.1";
}
So we could wrap this function in some utility component and use it. But if you are using cbsecurity
and only need this for registering unauthorized or unauthenticated access you can save yourself the effort of writing this component yourself.
cbsecurity
announces both a cbSecurity_onInvalidAuthentication
and a cbSecurity_onInvalidAuthorization
interception event. In the event data you will get the offending IP and more data, as documented in the coldbox-security docs. So by writing and registering an interceptor which captures these events, you could log these requests.
Unfortunately my client was more demanding. We also needed auditlogs for successful authentication, password reset request, 2 factor authentication and more. We could write our own function but since the cbsecurity module has some public getRealIP
function we decided to use this one, e.g.
authLogger.warn( "Dr. Evil failed from #cbsecure().getRealIP()#" );
So how about these bad guys? It is quite easy to spoof some headers or use some anonymous proxy. So don’t think these logged IP’s are 100% reliable. Furthermore, many visitors are hiding behind some company or internet provider firewall, so you will detect the IP’s of these machines instead of those of individual users. In most cases this is sufficient, because it still can give you enough data in case of abuse.
If you are using CommandBox as your server, you don’t even need to do this manually. Just enable the
server set web.useProxyForwardedIP=true
setting in your server.json and the CGI scope will automatically contain the client IP parsed from the headers. The reason this CommandBox setting is disabled by default is because you should only trust these HTTP headers (which can be easily spoofed) if you are being a proxy you trust that you know overwrites the headers with trusted values. For logging it’s not a big deal, but if you are controlling the security of your site by allowing users from certain IP ranges to access things, it makes a big difference.
One thing that just came up on our end in a Penetration Test was that we weren’t validating the value in the `X-Forwarded-For` header. The pen-testers were putting all kind of weird junk (mostly URLs) into that field. We had to add some validation to check the formatting. I am not sure what the “vulnerability” is, per say (as far as the pen-test goes); but, since it’s not a magical, trusted value (like cgi.remote_addr), they wanted us to lock it down more.