The Linux Page

Implementation of a secure log in via HTTP[S]

The following is a list of points one wants to follow in order to create a log in form and the necessary code in the backend.

Generate a secure Log In form (session id)

Each time you create a Log In form (or any form if that matters,) you should include a hidden session identifier in it. This will help you prevent users from posting to your server without first loading the form. This alone already prevents a large number of robots from flooding your server with totally useless POST commands1.

This concept is very simple: if you don't first go to the log in page, how could you log in at all?

The form will look something like this:

<form method="POST" ...>
<input name="login" size="32" maxlength="256" />
<input type="password" name="password" size="32" maxlength="256" />
<input type="hidden" name="formid" value="your-random-session-id" />
</form>

You must make sure to keep the session identifier in your database along with a date when you created so when the user attempt the log in he POST back to you and you can check that you indeed created that identifier and that it did not yet time out.

Even if you don't have a website form, the session identifier is important. The process goes like this:

  1. Send a log in request to the server
  2. Server generates the session identifier + date of request (date stays on server only)
  3. Client user is asked for his user name and password
  4. Send the log in information + session identifier
  5. Server checks the session identifier and date for validity
  6. Reply from server (true or false ONLY)

It is important that you avoid sending any detailed information of why the log in process failed. You probably want to log information on your server, just in case, but do not send anything more than TRUE or FALSE (0 or 1) as a result of the log in process.

NOTE

For a website server, the use of the HTTP_REFERER is not recommended, although it can be used in addition, some users setup their privacy level to such heights that you may not even get that variable set properly. Not only that, it's part of the HTTP header and can be set to anything by your dear hackers. The session identifier, on the other hand, makes their life quite a bit harder.

POST scheme

Make sure that any form that needs to send secure information uses the POST scheme. This means the data is incorporated in the HTTP packet and not on the URL after the question mark (?login=me&password=pwd1).

In PHP, that means you want to use the $_POST variable to gather the data sent by your form.

Packet size

The size of the log in and password information must be controlled (so should the session identifier.)

Whenever your server receives a POST packet, it should analyze the data for validity. This first means checking for the size of the strings. If too long or too short, you can fail immediately (although you may want to pause for a small amount of time to simulate a database access so hackers don't know that the data is that bad.)

So, assuming you access a log in of at least 6 characters and at most 256, and a password of 8 to 256 characters, you can test the sizes like this:

$login_len = strlen($login);
$pw_len = strlen($password);
$sid_len = strlen($session_id);
if($login_len < 6 || $login_len > 256 || $pw_len < 8 || $pw_len > 256 || $sid_len != 40) {
  sleep(100ms);
  return false;
}

You may want to use a completely different scheme for the sleep() mechanism. And if the length of the session identifier cannot change, testing for that specific size is best.

Now you may wonder why that is required if your software or browser is not allowed to send data outside of data with the correct sizes... That's quite simple: a hacker will write his/her own software and not care about your software limits. If that's the case, then testing on your server side is a really good idea.

NOTE

For a Browser form, you cannot impose a minimum limit with an <input> tag. Instead, you have to write some JavaScript code and hack the form action so the JavaScript is executed before the form is forwarded to the server. That way you can enforce all the sizes and other details such as whether a field includes a well formatted number instead of text, etc.

IMPORTANT NOTE

Note that you do NOT want to clip your variables. Doing something like a substr() or mid$() is not a good idea because only hackers will send you log in information of an invalid length. So "blah blah blah" should not match "blah bla" because you decided to only use the first 8 characters.

Packet data validity

You should also check the strings validity. For instance, no browser let you enter control characters in the log in and password input fields. Check that all characters are between 0x0020 and 0xFFFD. Also, you may want to forbid a certain number of things such as spaces at the begin and the end, or two spaces in a row.

Now, don't over do it because your usual users won't put up with it.

Connection encryption

Over the Internet, everything should be encrypted. Not really the case, but it should be2.

For a website, the easiest is to use HTTPS since everything is automatic in that case.

One way or the other, may sure your connection goes through some kind of SSL encrypted TCP/IP system.

This is really only necessary to send the log in and password. However, the form should also appear on a page loading with SSL encryption. This makes your users feel secure from that point on. Although the form action URL can send the user to a secure socket at the time they click on Submit, they cannot see that will happen when they have to enter the data.

The remainder of the transactions do not need to be over SSL if high security is not necessary afterward.

Log all accesses

Especially if you expect a large number of users logging in your system (millions every year) then you probably also want to log each log in attempt (failures and successes.) Failures should come with a log message explaining in details what was wrong. In our previous example, you could test each case separately and generate a very precise error.

if($login_len < 6) {
  log("username was too short.");
  return false;
}

This is a very effective way of finding problems when they arise later.

Remember that when you deal with a client/server system, it is very difficult to know what is happening unless you log a lot of information. The log() function should very quickly write to a text file including a time stamp and maybe a section name (i.e. the name of the area of your software logging information.)

Password encryption

On your server, you want to encrypt the password. At this time, the minimum is SHA 256. If you want to save it in text, it generates 64 characters. Keep ALL characters or two different password could otherwise match. Not a good idea.

However, remember that the SHA 256 function is not enough. You also want to use a salt of 2 or more characters. This is important because two different users may use the same password (password1 anyone?) and without the salt a hacker could determine that the two users have the same password. This means if they hacked one user, they can now access the other user account too without any extra work.

$salt = random_char() . random_char();
$encrypted = hash('sha256', $salt . $password);

The password can then be saved on your hard drive along with the salt that will be necessary to compare the password on next log ins.

SQL Injection

If you've read anything at all about security, you must have heard of SQL Injection. This means the user can write SQL code in the data he/she is submitting to your server. For instance, my log in name could be "DELETE FROM users;", just for fun. That would probably not work, but it gives you the idea.

To avoid problems, all data must be properly escaped. To make sure that always happens, you want to check every single string and number you include in your SQL statements. Most of the database system come with a set of functions to convert data safely for SQL statements. In C and PHP you get functions such as pg_escape_string() for PostgreSQL.

$sql = "SELECT * FROM topsecret WHERE username = '" . pg_escape_string($_POST['username']) . "'";

In either PHP or C++, the best way is to create an object that handles all the SQL functions. That way you can handle the string escaping within the object and not have to think about it each time you write an SQL statement. Drupal 7 has such an implementation. Drupal 6 and older version use a %s/%d scheme (a la sprintf(3C).)

If you decide to use a file mechanism instead, the same applies. For instance, a password like file uses the colon as a separator, so a username that includes a colon won't work right:

this:user:name:and:password

In this case you may want to replace the colons with %3A as we do on URLs, but don't forget to also change lone % in %25 or the user could either enter : or %3A and get the same results.

this%3Auser%3Aname%3Aand:password

I will also say: do NOT use the username or password as a filename. The same applies to filenames since the user could include a / or \ character in his name and other fun characters.

Error output

If you are programming for a browser, know that anything printed in stdout and often stderr will appear on the user screen. This means if an SQL statement fails and the whole statement appears in the error output, the end user is not unlikely going to see it. For a secure form, that's definitively not secure.

Also, remember that it is not because your statement is valid SQL and it will work each time. There could be an overload of the database and the connection fails. Your fail safe system may have lost a hard drive and that one statement generates an error at that one time, etc.

The best is to make sure you capture all error output and redirect it to your server logs instead.

SQL LIMIT 1

The SQL statement necessary to check whether a user is logged in will generally need exactly one result. Any SQL statement that only requires one result can use the LIMIT 1 parameter. This will prevent wasting server resources and possibly prevent too much data from appearing in the client's browser (in case you did not properly redirect the error output.)

If you scan a flat file, do the same: stop at the first match.

Administrator alerts

If you have a large organization, you probably want to have staff 24/7 watching for failures. This will help you very quickly detect hackers coming in an account.

It is very tedious though if you cannot have dedicated staff for that one aspect of your system.

Too many failures

Instead of alerts on each failure, you may want to do so only once a certain number of failures were reached. 3 is a usual number. At that point you can also lock the user account so the user cannot log in even if he/she is the rightful owner.

The account blocking mechanism could be based on the IP address of the user so if the hacker is attempting a log in from IP a.b.c and the real user is on IP x.y.z, the real user can still log in.

Website Administration

If you have administrators using your website, then you may want to look into a special mechanism for them to log in. This includes the following:

  1. Test the user acceptable IP addresses (i.e. home, laptop, work...)
  2. Ask a few additional questions whenever the user just logged in (i.e. what the color of your dog?)
    1. Rotate the questions
    2. Don't give logical answers (i.e. my dog is green)
  3. Put all administrative features in a separate folder and lock it with .htaccess
  4. Lock the administrative features with an additional basic oath
  5. Create a separate log in page for administrators that is locked from the wide public
  • 1. If you know why they do that, let me know because there is really no reason to send totally random data to a web server, is there?
  • 2. On that one, that's why I use a different password for each website where I register an account. That means if they save the password in clear and the connection is not encrypted, I at least don't give away my password.