Security
Web application security involves more than just passwords. But this document focuses on passwords and their related salts and cookies. (We hope to add "part two" soon about validating GET and POST data.)
This text is written for three reasons. One, to make sure that we understand what we are doing; two to inform others what we are doing with this code; three, in hopes that two other things result: people learn from it, or people contact us about any flaws or any improvements that can be made.
In THIS code there are two kinds of users, Admins and Visitors.
Salts
For protecting the Admin and Visitor passwords there are different salts for them. The salts are randomly generated, one for each, and will be different for each setup. (There is a way to manually override this for the user to chose his/her own salts.) The default salts are for SHA_512.
There are three salts, one for Admins and one for Visitors, which are in SHA_512 format. The third "salt" is just a mix of several random upper and lowercase letters and/or numbers and is used for encrypting cookies.
Admin Setup
During the Setup process the user enters a name and a password, and the three salts are generated. These are then written as PHP formatted data (as AD_NAME, AD_PASS, AD_SALT, VIS_SALT, COOKIE_SALT) to a file. The password becomes the output of the function:
$encryptedpassword = crypt($password,AD_SALT)
When the Admin code is next run it creates a Users database and stores in it a "user record" consisting of "name:encryptedpassword".
Admin Login
The Admin submits his/her name and password, ill-formatted names are rejected, then the name is looked-up up in the database, if it exists it's record is read. The user password is then compared with:
crypt($password,$encryptedpassword) == $encryptedpassword
and success is if they match.
Admin Cookie
If name and password are accepted two cookies get set. The cookies are set using the following data:
$name = user name $extra = user IP address $salt = COOKIE_SALT
The two cookies are set then as:
userid = $name userdat = base64encode(md5($name.$extra.$salt,TRUE))
Each time the Admin code runs it checks for the two cookies and compares them (if they exist):
$name = cookie('userid') $data = cookie('userdat') base64decode($data) == md5($name.$extra.$salt,TRUE)
Access is allowed if they compare true.
If a user accesses the site by a different network that provides a different IP address he/or will either have to have previously logged out or has to delete their cookies (which is what logging out does) and login again.
See the section on SECURITY for why there are two cookies.
Visitors
THIS code does not "register users" for posting comments. It provides a way to remember a visitor's name by allowing the visitor to provide a "code" (which is just like a password). There is a visitor database to store a visitor's name and code, like for admin users, just using the visitor salt:
$encryptedcode = crypt($code,VIS_SALT)
And when logging in the test is:
crypt($code,VIS_SALT) == $encryptedcode
Visitor Cookies
Visitor cookies are encrypted/encoded similarly to Admin users except, for us, we chose to not include the IP address in the data (so visitors can roam from network to network without having to log out/in), and different names are used. The data are:
$name = visitor name $salt = COOKIE_SALT
The two cookies are:
from = $name fromdat = base64encode(md5($name.$salt,TRUE))
Each time the code runs it checks for the two cookies and compares them (if they exist):
$name = cookie('from') $data = cookie('fromdat') base64decode($data) == md5($name.$salt,TRUE)
The visitor is considered logged in if they compare true.
Security
This section is about Admin account and cookie security.
If a user's computer is in the hands of another, and it's local password is compromised, and the cookies exist and are not expired and the person is using the same network connection, they have admin access to the site. (If their web browser is set to "save passwords" and the admin password has been saved...)
If a user's cookies are compromised (stolen), and somehow inserted into a different machine that has the same network connection, they have admin access to the site.
The reason for two cookies is that cookies can be forged. We had at one point just set a cookie of userid = name
after verifying a user's password, and then just checked that cookie for existence for subsequent visits. But if someone knew that that is all we did — and the code is available so anyone can know that — all one had to do was forge a cookie to be a logged in as an Admin. That is not good.
With a cookies pair, the name and the encrypted name, which are always encoded/decoded with a secret known only to us, no one can forge the encrypted cookie — unless they get a copy of the secret. Adding an IP address into the encrypted data just, in theory, slows down a cracker if they get someone's cookies.
If somehow the PHP file containing the Admin name and password and the salts was stolen, the thief would have to do some work to gain access. The user name is known. The password is encrypted and the salt is known, so a dictionary could be used to try and find the password. But a cookie can easily be forged to gain Admin access.
Preventing direct web access to the PHP file can be achieved by .htaccess, but even that is unnecessary if the PHP file contains just a number of defined constants. The PHP file could be placed below DocumentRoot to prevent the file being downloaded directly if FTP access to the site was compromised (social engineering to get the password or finding a password written down, however unlikely).
The leaking of the secret (salts) seems to be the main concern here.
Code Errors
Code can have errors that leave possibilities for "leaks" if there is any code that could possibly display data (by design or by exploit) that happened to contain usernames, passwords (encrypted or clear) or salts. All of these can be prevented by never storing any sensitive data as variables, and only using defined constants where appropriate in the code.
The having viewable highlighted or PHPDoc created source code is another possible leak. Any such source code documentation must be configured to never process any file with sensitive defined constants.
Defaults
Having defaults for any sensitive data leaves open the possibility for people to just use them as is. If possible, and ideally, no sensitive data should have defaults stored anywhere. Users must have to create them themselves, either by instruction of how and where to edit a file or through an HTML form. And these data should then be stored in a protected file (or, for example, in a PHP file as defined constants).
If there has to be a default password, like a router for example — although I am speaking about website applications in general here — the code should have a "You must change this password before continuing," mode.
In a related area, files too have defaults that should be able to be changed. My website logs show constant fishing attempts for known default file names such as login.php
, signup.php
, wp-admin/
, etc. The list is a long one. Being able to change your website's administration page name is just one more preventative measure to take (unfortunately, many large and popular web applications have hard-coded files name throughout many files).
Of Course There Is More
This is just a "first draft" and we plan to expand it as mentioned in the introduction.
Notes
1. I took this from a note on PHP.NET.
<?php
function randomstring($len) {
$s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return substr(str_shuffle($s),0,$len);
}
?>
2. Getting the SALT of course, barring any software errors that could leak it or allow it to be stolen, would be near impossible if correctly stored on the server. However, humans can make mistakes. We thought of putting the encrypted password (less the salt portion) into the cookie which would prevent the cookie forgery, but that means transmitting it back and forth over the Internet and we have to think more about the ramifications of that.
3. Pretty standard stuff to many, of course, but we all had/have to learn this stuff at some point.