I saw an article on dzone.com today that talked about ways to restrict logins after a number of unsuccessful attempts. I thought I'd show a quick demo of how to do this in ColdFusion. This method is not perfect and I'll talk about alternatives. I will also link to the original article so you can see how it was done there.
So - the concept is simple. If a user tries to log on N times, and fails, you want to prevent them from logging in again. This prevents a hacker (or script kiddie) from spending all day trying to guess a user's password.
You don't want to block the user forever of course, but just for a period of time. So let me show you the code I used for this demo, and then I'll talk about it. (A quick note - I wrote a one page demo for this post. On a real site this code may be broken up a bit into multiple files.)
<cfapplication name="test" sessionmanagement="true" clientmanagement="true">
<cfparam name="session.loginattempts" default="0">
<cfif structKeyExists(form, "logon") and structKeyExists(form, "username") and structKeyExists(form, "password") and session.loginattempts lt 3>
<cfif false>
<!--- log the user in --->
<cfset session.loginattempts = 0>
<cfelseif true>
<cfset session.loginattempts = session.loginattempts + 1>
</cfif>
</cfif>
<cfif session.loginattempts lt 3>
<cfoutput>
<form action="#getFileFromPath(getCurrentTemplatePath())#" method="post">
</cfoutput>
username: <input type="text" name="username"><br>
password: <input type="password" name="password"><br>
<input type="submit" name="logon" value="Logon">
</form>
<cfelse>
Sorry, but you have attempted to logon too many times.<br>
You are either very stupid or a hacker. Please go away.
</cfif>
This is pretty self-explanatory. I initialize a session variable called loginattempts. (This would normally be done in the onSessionStart method of your Application.cfc file.) When the user attempts to login and they fail, I increase this value.
Later on I notice if the loginattempts value is too high and I display a message instead of the login form. As a side note - it occurred to me while writing this entry that I checked the value on display but did not check it when the form was submitted. I modified my initial CFIF to add that last clause. That way the user can't just hit "Back" and continue to try to login, or write their own form on their desktop and post from there.
So - this works easily enough but has a few problems. First - it uses session variables. It takes me all of two seconds to clear those values by using the Firefox Web Developer toolbar. Or I could switch to the Browser that Shalt not be Named. This technique would stop some script kiddies, but not someone who was very serious.
Instead of using the session scope (or some other cookie), you could check the IP address of the person making the request. This could be stored in a simple struct where each key is the IP address and the value is the number of login attempts. However, you would also need to store the time of the last login attempt. The nice thing about the session variable is that it will automatically go away when the session dies, thereby allowing the user another chance to login.
This method would not be full proof as you can't always trust the IP, and if the server were to go down you would lose the Application scope anyway. (Of course, it's hard to hack into a site that is down. It would only matter if your site actually came back up.)
One more idea - and what the original article did (linked below) - is to log the attempts to a database. This gives you the added benefit of being able to do tracking later on. If you see numerous daily failures from one IP, it is probably something you want to check out.
Anyway - enjoy - and readers - feel free to share how you did this in your own applications.
The original dzone.com article: Blocking access to the login page after three unsuccessful login attempts
Archived Comments
I do basically the same thing, with the distinctions that you already make about setting the session variable on form submit. A side note, if you permit users to log out of your application, and typically clear session values on logout, remember not to clear your session login count when you log out someone who has failed.
To make things even harder to figure out, I check for the logout before anything displays on any page, and redirect to a lockout.cfm page on failure. I usually enforce password complexity and do case-sensitive checks, so I typically just want to slow down brute force attacks.
I set up one application where I flagged the user for lockout in the database after a session lockout; that was not so good, though - it was too easy to create denial of service attacks by locking everyone out.
I log attempts in a db from both an IP address and against a username. Because I'm logging against a username it can count failed attempts even if the hacker clears their cookies/session or rotates their IP address. Of course when the genuine user tries to log in it may block them if their account has been attacked, but this is a security measure and it's in their interest to protect their account. Banks would do the same thing if they detect attempted fraud on your account.
Logging to a db can also detect slow attacks that happen over a long period of time - scripted ones that may only try a few attempts per hour per username over weeks so not to raise the alarm. How far you take this depends on the value of the data you're protecting and the consequences of a hacker getting in. Our logs do take up a lot of disk space but it was a cost built in to the budget and we can change the purge date if they become too big.
Another idea I'd like to share is to ask a security question at login if a user tries to log in from a different IP address or computer from their last successful login. You can do this by checking for the presence of a cookie deposited from their last login (or an encrypted value in a cookie for higher security). This will only infrequently inconvenience a minority of users (e.g. those on a dynamic IP or periodically clear their cookies) but it will show hackers that their chances of a brute force attack are ridiculously tiny and they're better off hacking someone else's site.
I also log failed attempts from a given IP address, and if I get 10 consecutive failed password attempts to any username from a given IP, I ban that IP from further attempts until I manually release it. I do give the user a message telling them that they will need to contact their customer service rep befure any further attempts can be made, but this just makes me feel better about script kiddies not being able to brute force any of my pages.
It just occurred to me, reading Gary and Justice's comments - we could put Captcha images on login pages. They wouldn't have to be very long at all - just a couple or three characters - and that would pretty much bring brute force to a halt. What do you think?
Edward, nice idea. That would help deter scripted attacks (programatically and pyscologically) but I would be concerned for users who can't visually work out the captcha text (for mental or optic reasons). It's also slightly discouraging to 20-20 vision users, some who would log in at least daily. I know typing in 3 extra chars is nothing (perhaps taking Joe Average 3 seconds longer) but you know how funny users can be about these things!
I'm with Gary. I do think this would help, but would be a royal pain in the rear.
However - I have seen sites that only use CAPTHA after you fail to logon. That would strike a good balance don't you think?
@Justice:
Be careful using the IP address as a sure fire way to identify someone. Most companies will use a proxy or a router which make all outbound traffic coming from that location use the same IP address. This means blocking a single IP address could erronously block many users.
Also, a hacker won't have any problem changing the IP address of his attacks.
While collecting the IP address is all good and well, just don't think it's a surefire method. I'd also not block an IP, but just the specific account, as you could end up locking out legitimate users.
Ooo, Ray, *that* is a great solution! I'm going to try that TODAY!
Ray,
You're right about the session variables, bad idea. Never use any variables under the control of the user for such information, for this to work it is *required* that the number of attempts be stored with the user record being checked.
Another problem with lockouts is that they can cause a massive denial of service. So, a few things to think about (directed at many of the comments):
1) Error messages should never include information about whether the username or password was invalid. "Inavlid login" is sufficient and doesn't tell the attacker if he has a valid username.
2) Same thing for lockout messages, they are never sent to the users browser. If the user is locked it must be communicated via email, otherwise the attacker knows he has a valid user.
3) Lockouts that expire only slow down attackers somewhat, they don't prevent attacks from succeeding. Combined with bad error messages, an attacker will just set up a multithreaded attack scenario working on many users at a time, waiting for locked users to be unlocked.
4) Bad passwords make this easy. Enforce high password complexity either through long passphrases or complex passwords and force regular password rotation. Most accounts are broken through this kind of brute-force attack.
5) Logging IP addresses is pretty useless. Botnets have hundreds or thousands of machines with different IP addressed. If you block based on IP, you may DoS an entire company or service provider.
6) Many CAPTCHA implementations are easily broken by automated machines. Of course I could just hire cheap overseas labor to do it manually, if I had enough motive. (Yes, its been done.)
So, what's the best way to handle this IMHO? Progressively longer lockouts combined with email notification to the user, eventually resulting in a hard-lockout which must be reset manually by an admin.
5 attempts. Lock out 15 min and email user.
3 attempts. Lock out 30 min and email user.
2 attempts. Lock out 6 hours and email user.
1 attempt. Lock out 24 hours and email user.
1 attempt. Hard lock out. Email user.
This gives you 30+ hours to find an attack in progress (you are monitoring logs, right?) and for users to be informed that they have either locked themself out and it will be reset in X minutes or that an attacker is trying to brute force their account. This will significantly slow down an attack and any passwords which are not trivially weak will remain protected.
Of course, if I can perform SQL Injection on your DB and retrieve the passwords or use XSS to socially engineer your users, all of this is a moot point.
"Of course, if I can perform SQL Injection on your DB and retrieve the passwords or use XSS to socially engineer your users, all of this is a moot point."
Well, all of us are using cfqueryparam and valid ULR/Form checking, right?
-sigh- (If only that was true...)
If only Ray. Then I'd be out of a job!
FWIW, CFMX doesn't have good protection from XSS. You have to write your own code to ensure escaping of all XSS chars:
<
>
(
)
#
&
'
"
And that's for output encoding... input validation is another challenge altogether.
The problem about a denial of service attack locking everyone out is when you make the login id's generally available, such as displaying the posters id in a forum post. The trick is to use an additional display nick that can be used for internal site traffic and an id that is only used for the login.
You can really keep the script kiddies at work by setting up field name randomization. You generate a random name for both the id and password, store those in the session. On the return trip, you compare the session.passwordField with a StructKeyExists(Form,session.passwordField)test, you get the idea. A script kiddie can get around this, but that means extra work by having to process the returned form.
Also don't forget that anyone hacking into your site is committing a crime, so logging IP's, while expensive, can be very worthwhile when you can turn the logs over to the FBI.
If you really want to have fun, setup a honey pot with bogus data. Have it alert you at what you can pretty much be sure is a hack attempt. Let the idiot log in, have a message that they are logged on, let them "Change" their email (as in stupid enough to provide a way to contact them) and then log everything and learn a little bit about what the hacker is trying to do.
Parsing rotating field names is trivial for anyone who is dedicated.
Guessing passwords is not a crime. IP Addresses which are outside the country and/or part of a botnet may not be traceable to the owner. So that's a moot point.
Um, guessing a password is a crime if you use it... If you access something that you are not supposed to.
18 U.S.C. 2701.
Unlawful Access to Stored Communications
§ 2701. Unlawful Access to Stored Communications
(a) Offense.--Except as provided in subsection (c) of this section whoever–
(1) intentionally accesses without authorization a facility through which an electronic communication service is provided; or
(2) intentionally exceeds an authorization to access that facility; and thereby obtains, alters, or prevents authorized access to a wire or electronic communication while it is in electronic storage in such system shall be punished as provided in subsection (b) of this section.
I agree that relying on IP addresses is too problematic, since IP addresses could cover multiple users. And as Ray says, session-based cookies are pretty easy to clear. So how about a combination of cookies and cache tracking?
<A HREF="http://www.mukund.org/blog/...">http://www.mukund.org/blog/...
If you manage to embed a unique session ID in a cached JavaScript file, you can use the JavaScript file to send the session ID back to the server (say, via AJAX or with something as simple as an image preload). Since the JS file is cached, it isn't cleared as easily as cookies can be. So you can rely on cookies and on cached files in tandem to at least try to keep session identification working.
Ray,
I don't understand the lines:
<cfif false>
<!--- log the user in --->
<cfset session.loginattempts = 0>
<cfelseif true>
<cfset session.loginattemps = session.loginattempts + 1>
</cfif>
How would the first CFIF in this sequence ever execute? If it never executes, what purpose is it?
This was just a demo, so I wrote to make it so that your logon ALWAYS failed.
"I agree that relying on IP addresses is too problematic, since IP addresses could cover multiple users."
You could check if CGI.HTTP_X_Forwarded_For exists - I can't say I'm entirely sure how that works, or if it's reliable, but I believe if the punter is using a proxy, you'll skip that ip and see their 'real' one..