I am wanting to re-write my login scripts for clients websites to make them more secure. I want to know what best practices I can implement into this. Password protected control panels are in their abundance, but very few seem to impliment best practices in terms of code writing, speed and security.

I will be using PHP and a MYSQL database.

I used to use md5, but I see that sha256 or sha512 would be better (together with secure hash and salt).

Some login scripts log the ip address throughout the session or even the user agent, but I want to avoid that as it isn't compatible with proxy servers.

I am also a little behind the best practice in using sessions in PHP 5 (last time I read up was PHP 4) so some best practices with this would be helpful.

@Chris CodeReview help with code clarity and the like. You are definitely not supposed to go to them for security. If anything, they will almost always remind you not to "roll your own" which is basically the same answer as he's going to get in any programming community on SE.
–
AlternatexMay 20 at 11:59

5 Answers
5

The best think is to not reinvent the wheel. But, I understand, in PHP world it may be difficult to find a high quality component which already does that (even I'm pretty sure that the frameworks implement such things and their implementations are already tested, solid, code-reviewed, etc.)

If, for some reasons, you can't use a framework, here's some suggestions:

Security related suggestions

Use PBKDF2 or Bcrypt if you can. It's done for that.

Rationale: both algorithms can make the hashing process arbitrarily slow, which is exactly what you want when hashing passwords (quicker alternatives mean easier brute force). Ideally, you should adjust the parameters so that the process becomes slower and slower over time on same hardware, while new, faster hardware is released.

If you can't, at least don't use MD5/SHA1. Never. Forget about it. Use SHA512 instead, for example. Use salt too.

Rationale: MD5 and SHA1 are too fast. If the attacker has access to your database containing the hashes and has a (not even particularly) powerful machine, brute-forcing a password is fast and easy. If there are no salts, the chances that the attacker finds the actual password increases (which could make additional harm if the password was reused somewhere else).

Rationale: calling a function provided by the framework is easy, so the risk of making a mistake is reduced. With those two functions, you don't have to think about different parameters such as the hash. The first function returns a single string which can then be stored in the database. The second function uses this string for password verification.

Protect yourself from brute force. If the user submits a wrong password when she already submitted another wrong password 0.01 seconds ago, it's a good reason to block it. While human beings can type fast, they probably can't be that fast.

Another protection would be to set a per-hour failures limit. If the user submitted 3600 wrong passwords in an hour, 1 password per second, it's hard to believe that this is a legitimate user.

Rationale: if your passwords are hashed in an insecure way, brute force can be very effective. If passwords are stored safely, brute force is still wasting your server's resources and network bandwidth, causing lower performance for legitimate users. Brute force detection is not easy to develop and to get right, but for any but tiny system, it's totally worth it.

Don't ask your users to change their passwords every four weeks. This is extremely annoying and decreases security, since it encourages post-it-based security.

Rationale: the idea that forcing passwords to change every n weeks protects the system from brute force is wrong. Brute force attacks are usually successful within seconds, minutes, hours or days, which makes monthly password changes irrelevant. On the other hand, users are bad at remembering passwords. If, moreover, they need to change them, they will either attempt to use very simple passwords or just note their passwords on post-its.

Audit everything, every time. Store logons, but never store passwords in audit log. Make sure that the audit log cannot be modified (i.e. you can add data at the end, but not modify the existing data). Make sure that audit logs are subject to regular backup. Ideally, logs should be stored on a dedicated server with very restrictive accesses: if another server is hacked, the attacker will be unable to wipe out the logs to hide his presence (and the path taken during the attack).

Don't remember user credential in cookies, unless the user asks to do it (“Remember me” check box must be unchecked by default to avoid human error).

Ease of use suggestions

Let the user remember the password if she wants to, even if most browsers are already this feature.

Don't use Google approach when instead of asking for user name and password, the user is asked sometimes for password only, the user name being already displayed in a <span/>. Browsers can't fill the password field in this case (at least Firefox cannot do that), so it forces to logoff, then to logon with an ordinary form, filled by the browser.

Don't use JavaScript-enabled popups for logon. It breaks browsers password-remember features (and sucks in all cases).

Let the user enter either her user name or her mail address. When I register, sometimes the user name I want to enter is already taken, so I must invent a new one. I have all chances to forget this name in two hours.

Always keep a link to "Forgot my password" feature near the logon form. Don't display it only when the user failed to log on: the user who don't remember her password at all have no idea that she must submit a wrong one in order to see the "Forgot my password" link.

Not come across bcrypt. What's your reasoning for recommending that? Why not md5/sha1? Thanks for the rest of the suggestions!
–
baritoneukMay 2 '11 at 17:02

The reasoning is simple: bcrypt is done by people who are highly qualified. It's also tested, reviewed, etc. So there is no reason to implement the same thing yourself. It's like creating your own cryptography algorithm for your website. *"Why not md5/sha1?": see for example en.wikipedia.org/wiki/MD5#Security
–
MainMaMay 2 '11 at 17:13

4

bcrypt is slow and as mentioned implemented by professionals. md5 is a hashing algorithm not an encryption which are not quite the same thing. Hashing a file fast is good, md5 would be used here... hashing a password need not be so fast, bcrypt can help here. Reason I say this is that brute force becomes more difficult when the crypt algorithm is "slow".
–
ChrisMay 2 '11 at 17:54

Just regarding the bit about frameworks. I've tended to avoid frameworks in php. However, I assume it could be possible to use one just in the case of a login script. It would be good to discuss which frameworks implement the best practices. Our perhaps a custom solution is best.
–
baritoneukMay 2 '11 at 18:12

1

@baritoneuk: minimum is cool. But I can't count how many sites I've been to that had restrictions like maximum lengths, no non-alphanumeric characters, etc. Bewildering, if you're going to hash the password anyway - which makes me worry that they are not, they're just storing it in their database in the clear.
–
Carson63000May 2 '11 at 21:28

Your site should use HTTPS. At no point should you present a login page or accept logins from a non-encrypted connection.

Your cookies should be restricted to HTTP-only and be restricted to secure connections if you use HTTPS.

The login process should take no less than 2 seconds (1 if you think 2 seconds is too long). Use a secure hash when storing and verifying passwords, and use a salt that is harder to guess. Use bcrypt if possible. Otherwise, use some other kind of iterated hash.

Never ever compose your database queries in any way that requires using functions like mysql_real_escape_string. Never use string concatenation to craft your query. Use prepared, parametrized queries. Most, if not all, DB drivers for PHP support it. If you don't know how to do it, spend some time learning how to prepare, bind, and execute using whatever DB you're using. It's the only sure way to guard yourself against SQL injection. PDO supports this.

Encourage your users to not use the same password they use for their email. Remind them that if your site is compromised and if they use the same password in both places, someone can hijack their email.

+1 for HTTPS, since there is more and more traffic through public WiFi networks. But I disagree with the point 3: 2 seconds is too long from the users point of view. 0.5 s. is just fine, especially if other anti-brute-force techniques are used.
–
MainMaMay 2 '11 at 16:52

I'm confused on point number #4. How do you escape the convention of escaping data with mysql_real_escape_string? All user submitted data should not be trusted and filtered accordingly.
–
chriswMay 2 '11 at 17:06

@chrisw: Yes, all user input should be handled as if it's malicious. But when you use parametrized queries, it is, bar none, the best way to stop SQL injection. If you're not using prepared and parametrized queries, you are vulnerable to SQL injection. mysql_real_escape_string is false security - it's not a guarantee. Parametrized queries are.
–
greyfadeMay 2 '11 at 17:39

I agree now after doing some further reading into it. The function: mysql_real_escape_string is generally safe for the most part, I wouldn't consider it a HUGE liability, but I can see how using the prepared statements are much more efficient.
–
chriswMay 2 '11 at 17:54

Use a salted one-way hash (preferably SHA512 using http://au2.php.net/manual/en/function.hash.php)... that way, if someone does hack your database, even if they know the salt, they cannot extract the passwords (without a rainbow table, which would be looong for SHA512).

Use HTTPS.

Don't send username/password confirmations via email back to the user when they register.

If you allow cookies for 'remember me' - add an extra login to access administration and profile editing functions.

If a user gets a password wrong, don't say they got the password wrong (say they got the 'username or password wrong' at all times) - that way, less clues on what are valid usernames.

Try and implement screen name versus login - that way, less users will display their login name on user listings.

Completely agree with the bit about not sending emails in plain text by emails. I have lost count of the times web sites do this. If you have 1 password for all sites (which luckily I don't) then potentially all websites are compromised. Such websites probably store passwords in plain text too. sigh
–
baritoneukMay 3 '11 at 8:25