8 Tips For A Secure Login Script/Admin Panel
After reading the title you may say to yourself “Oh no, another <<secure login script>> article! Aren’t there enough already online?”. Yes there are, but unfortunately many tutorials (if it’s appropriate to call them so) only show you how to write SQL Injection free code. But that isn’t enough. What about brute force (dictionary, hybrid) attacks? Or how about making your admin panel (user panel) CSRF free? Well this article will try to deal with those issues too.
Tip 1: Login Page/Authentication
Let’s start with the basic example, which you can find in all those so similar tutorials… First of is the login form:
<form action="login.php" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="submit" name="Login" />
</form>
Very complex html code, I know. It took me around 10 minutes to write it. Now for the php script that does all the work. NOTE: In this example and all to follow we always assume a mysql connection has been made and that sessions are started.
$username = mysql_real_escape_string($_POST['username']);
$password = sha1('salt'.$_POST['password']);
$result = mysql_query(
"SELECT id
FROM users
WHERE username='$username' AND password='$password'
ORDER BY id"
);
if(1!=mysql_num_rows($result)) {
echo 'Login failed, username or password was wrong!';
exit;
}
$_SESSION['auth'] = true;
$_SESSION['username'] = $username;
$_SESSION['id'] = mysql_result($result, 0);
echo 'Logged in!';
If everything is clear till now than we can move onward… (with this first section I’ve done a summary of all the secure login scripts tutorials)
Tip 2: Bruteforce?
Such a rudimentary attack that people almost forget about it. And most of the time it can be invisible to the websites administrator because POST requests aren’t logged by default. You may remember this from my article Logging the HTTP requests! But I’m not going to use that, because I want to keep all this simple, as simple as possible. So then, we will assume we got a table which is cleared daily (cron?) and stores only the username in the table. One columned table. (I told you that I’m gonna keep it simple) Then the following lines would be also in the login script (before the login query):
$counter = mysql_result(
mysql_query(
"SELECT COUNT(*)
FROM attempts
WHERE username='$username'"
),
0
);
mysql_query("INSERT INTO attempts VALUES('$username')");
if($counter>=10) {
echo 'Bruteforce attempt detected or more than allowed logins per day exceeded!';
exit;
}
As you can see the drawback for this is that while protecting the users from bruteforce attacks, it may also (and it’s likely) to lock them out. Luckily for us there is a solution for the problem. We add another column to our table attempts of type char(2) in which we store the current hour. Basically we limit the number of logins per hour.
$counter = mysql_result(
mysql_query(
"SELECT COUNT(*)
FROM attempts
WHERE username='$username'
AND hour='".date('G')."'"
),
0
);
mysql_query("INSERT INTO attempts VALUES('$username', '".date('G')."')");
if($counter>=3) {
echo 'Max number of logins per hour exceeded!';
exit;
}
Now it looks a little better, but still it misses something…
Tip 3: IP Ban List
You could use ban lists for various reasons, from allowing “known attackers” to blocking access via proxy. Generally people tend to use .htaccess files for blacklisting, but I will stick to PHP and MySQL for the example. NOTE: On medium to large websites a database (MySQL) – backend (PHP) application would stress pretty much the database. The following code should be inserted before the max number of logins per hour check.
$max_counter = mysql_result(
mysql_query(
"SELECT COUNT(*)
FROM attempts
WHERE username='$username'"
),
0
);
if($max_counter>9) {
mysql_query("INSERT INTO banlist VALUES('".$_SERVER['REMOTE_ADDR']."')");
}
And the following line on the first line of the script.
if(1==mysql_num_rows(
mysql_query(
"SELECT 1
FROM banlist
WHERE ip='".$_SERVER['REMOTE_ADDR']."'"
)
)) {
echo 'Your IP address is in the ban list!';
exit;
}
Of course you could dump the IP Address list from time to time from the database, but the following solution is a better one because it does not involve high stress on database.
Tip 4: reCAPTCHA
CAPTCHA is a good way to protect a web page (login page, important forms) from bruteforce and CSRF attacks. But we are not talking about self made CAPTCHA’s, you don’t have to reinvent the wheel… in a cubic form. That’s right, most of those who write there own CAPTCHA’s tend to make them weak or impossible to use (yes, Rapidshare’s CAPTCHA is one of those). You should use a good and public CAPTCHA, like reCAPTCHA. Also don’t over use them, you have to think of CAPTCHA’s like door keys. It’s very good to have one to your entrance door, but having to unlock the door for every room can be quite annoying, if not frustrating.
Tip 5: Encrypted login information
You either should use SSL or (if not available) frontend encryption (hashing in our case). A good example of such an implementation would be userAtuh’s one. You can find available md5/sha1 hashing library’s written in javascript around the web. One such an example is Paul Johnston’s Javascript MD5 library which is stated to be used by Yahoo on several non SSL pages. Can’t tell if it’s true or not, didn’t check it out, but for those who know Trilulilu (especially Romanian comrades) I can tell you that they implemented it in their flash player, for “encrypting” the source of their media (did I say too much?=).
Tip 6: Legitimate Requests (CSRF free)
As mentioned before excessively using CAPTCHA may be annoying, except in the case that the admin panel is designated for only one person (yourself) and you can bare completing on every important request a CAPTCHA field. For the other cases you could take a similar approach. On request of a page with an important form you could generate the token in the following way.
$_SESSION['token'] = sha1(rand());
And inside the form you could place it like…
<input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>" />
While in the form processing page you should deal with it as in the following code.
if($_SESSION['token']!=$_POST['token']) {
exit;
}
$_SESSION['token'] = sha1(rand());
Also a better way would be by using mt_rand() and seeding it with a secret method. Like for example the sum of the numbers from the session id.
Tip 7: Protect your includes/secure pages
If you develop your web application in a similar manner as myself than probably you include the admin pages, rather than use one single over bloated file. And it’s a common mistake to not protect those files from direct access. I’ve seen cases of obscure naming of the files, which are not secure if the attacker can get directory information in a way or another. Although are very interesting. Why? Because security by obscurity can be very fun an creative, even useful sometimes if build on top of a secure design. Like for example when using a MySQL database (version 4, so no information_schema database available) and obscuring the table names. Coming back to our subject, you could protect your included files by using the following line of code.
if(!defined('PROTECTED')) { exit; }
While defining the PROTECTED constant in the script which includes the files (which we assume you protect). You can secure your main files (those who include files) in a similar way.
if(!$_SESSION['auth']) { exit; }
define('PROTECTED', true);
Tip 8: Password Change
When making a password change functionality don’t forget to ask the user for their current password. Many think that if a user is logged in and no CSRF could take place they are safe and thus don’t need to ask them for their current password. It’s wrong to think like that because Session Riding attacks are more frequent than you think. And it’s not such a big deal of doing this, it only takes an extra where clause in the update syntax.
$old_password = sha1('salt'.$_POST['old_password']);
$new1 = sha1('salt'.$_POST['new1']);
$new2 = sha1('salt'.$_POST['new2']);
if($new1!=$new2) {
echo 'Your new passwords do not match!';
exit;
}
if(mysql_query(
"UPDATE
SET password='$new1'
WHERE id='".$_SESSION['id']."'
AND password='$old_password'"
)) {
echo 'Password successfully changed!';
}
else {
echo 'Password couldn\'t be changed!';
}
Tip 0
This are the most common approaches which should be taken when coding login scripts/admin panels. Also they are the most common neglected aspects. Kind of paradoxical in a way. And that;s not all, if we think of HTTP Redirects which by the way haven’t been treated because latest versions of PHP protect you against HTTP Response Splitting. Another thing I didn’t treat was XSS, which I think (and maybe you would approve) doesn’t make a part of this subject, it concerns the way you implement functionality in your pages. I often wonder why even give power to the user, like for example in a comment form. Why give him even the ability to post link (and convert them to link)?
Anyway, as you can see I didn’t post a ready to use script/class because I wanted you to understand the concept behind login/admin panel security. Not just copy-paste the code and cross your fingers that it will work.
UPDATE: On Ronald’s suggestion I replaced the md5 hashes with sha1 hashes, even salted them. Also modified the MySQL code to order the result by id, just in case there where two accounts with the same login information (which shouldn’t, if you properly did the checks in the registration page, if uses any).


hi,
I like the article and now I understand the concepts.
Would you consider making the whole script available?
it’d be better to use and improve on as a whole.
Regards
I would also like to request if you could make whole script available. Its much easier to understand if you have the whole picture in front of you otherwise one would not know if he is implementing it in the right way.
regards.
I must say that this is a great article!
Thanks for sharing!
I am a newbie and really liked this article. I would also like to request if you could consider making the script available for download. It will be easier to then modify it to fit to our needs and understand the whole thing in a better way.