Tidbits @ Kassemi

A collection of opinions, thoughts, tricks and misc. information.

Monday, August 29, 2005

 

ARGH! (Learn good programming practice early part two)

First, let me inform everybody that I'm looking for a place to host files for this blog. It's difficult to take code that's posted all over a page, and blogger doesn't help much, as they take out some of the markup, so I'm never sure if the code I do post actually works when you copy and paste it. The solution is just to give everybody a tar.gz'd or tar.bz2'd file... I'm also going to start working on an AJAX syntax highlighter for the code here, that would search through the page for code elements and highlight them appropriately. I already have such a system for help information on my current project, so changing that wouldn't be that hard. The only problem is I need a place to put a javascript and php file, so I can include that functionality here.

Now to the ARGH! portion... When I first started programming in PHP (about a year or so), I took a look at a sessions tutorial so I could create a login page for my first project, a forum that allowed infinite category creation and customization... Pretty nice, but looking at it after I had done some work, I realized that it would never be commercially suitable. The tutorial, which I wish I could find, said that all I needed to do to provide a login for the user was something like this (I use my mysql class for this stuff, but you can figure it out):


session_start();
$db->query(sprintf("select password from users where username='%s'",
mysql_real_escape_string($_POST['username'])));
if($db->result() == $_POST['password']){
$_SESSION['username'] = $_POST['username'];
}else{
echo "ERROR!";
}


That was great. All I needed to do was present the user a form with a password and username, attach something like the above to it, and:


/* WARNING: THIS IS NOT GOOD. DON'T DO. JUST DEMONSTRATING SOMETHING */
session_start();
if(isset($_SESSION['username'])){
echo "I'm a valid log-in!";
}


Because sessions are the most secure fucking things on the planet, right? ARGH!

So I started programming that way for a while, until everything I'd done used that technique to get what user was logged in, whether the user was logged in, and whether the user's login was valid... Soon enough my code base was polluted, and I decided I'd fix it later, putting the date off forever. Sure enough, the day I had to fix it was today! I decided I didn't want to mess around with SSL for this site, which doesn't really have information that needs that much protection, but I didn't want any packet spy to get the passwords for my users when they loaded the page up on a public network. So I decided to go with a javascript md5 hash solution...

I create a database:


challenge | ip | created


Which holds a randomly created variable, the challenge, the ip that requested the challenge, and the time in which the challenge was created... I actually used an additional column, `type', as I want to have those image challenges sent too (remember yesterday's cool spirals?).

So, whenever a user wants to log in, they are given a form that has the following inputs:



A challenge is randomly created using this code:


function genRandStr($length){
$st=array(48,65,97);
$en=array(57,90,122);
$chr="";
for($i=0;$i<$length;$i++){
$rnd=rand(0,2);
$chr.=chr(rand($st[$rnd],$en[$rnd]));
}
return $chr;
}


This is sent to a hidden field on the form, "challenge."

When the user fills out the username, password, and characters in the image, the following is executed:


var challenge = document.getElementById('challenge').value;
var username = document.getElementById('username').value;
var password = document.getElementById('password').value;
var challenge_image = document.getElementById('challenge_image').value;
var final_hash = hex_md5(password);
final_hash = hex_md5(final_hash+challenge+username);
location.href = "login.php?username=" + username + "&challenge_image=" + challenge_image + "&secured_pass=" + final_hash + "&challenge=" + challenge;



The md5 library for javascript that I use can be found here:
http://pajhome.org.uk/crypt/md5/index.html

Basically, I convert all the password, along with the challenge, to an md5 hash... Which gets sent over the network to my login script again. This means that the password itself is not sent, meaning a packet sniffer can't get a hold of them. The packet sniffer does see the challenge string, though, but it still can't do anything with it... Brute forcing that combination would take some time...

login.php takes a look to see if all of the information is in order, and then does the following:


/* The type is because this is the challenge image string... 1=challenge image, 0=login challenge */
$connection->query(sprintf("select * from security_challenges where ip='%s' and type=1 and challenge='%s'",
mysql_real_escape_string($_SERVER['REMOTE_ADDR']),
mysql_real_escape_string($_GET['challenge_image'])));
if($connection->numrows() == 0){ /* They didn't type the challenge image string in right */}


$connection->query(sprintf("select challenge from security_challenges where ip='%s' and type=0 and challenge='%s'",
mysql_real_escape_string($_SERVER['REMOTE_ADDR']),
mysql_real_escape_string($_GET['challenge'])));
$challenge_db = $connection->result();

if($connection->numrows() == 0){ /* We couldn't find the challenge that they said they used */}

/* Get the password, we're doing this the same way I did before */
$connection->query(sprintf("select password from br_registered where username='%s'",
mysql_real_escape_string($_GET['username'])));

if($connection->numrows() <> 1){ /* The username wasn't found */}
$password_db = $connection->result();
$hash_compare = md5($password_db.$challenge_db.$_GET['username']);

if($hash_compare <> $_GET['secured_pass']){
/* The hash we created with the javacript and the hash we just used to check the js hash didn't
* match. Do not allow the user access with whatever routine you put in here. */
}else{
/* The hashes DID match */
/* This is a little more secure. We're not holding the password in the session,
* but another hash. $secure_global_pass is a secret phrase that should get changed
* every two weeks or so. */
$password_secure_compare = md5(md5($password_db).$secure_global_pass.$username);
$_SESSION['username'] = strtolower($_GET['username']);
$_SESSION['password_secure'] = $password_secure_compare;
/* Do what you will to give the user access. Forward to another page, whatever.... */
}



Great. So we're able to get a user logged in securely. The image bit might be a little excessive, but I like it, so I'm going to use it. If I get a single complaint from a user, it'll come right down... But how am I now getting the username and checking that the username is valid? I include this file with everything I do that requires a login... "logincheck.php":


function username(){ /* Return the username of the logged in user. */
global $secure_global_pass;
/* Get the login information, if it's stored in a cookie or in an active session */
if(isset($_COOKIE['your_site'])){
@$username = $_COOKIE['your_site']['username'];
@$password_secure = $_COOKIE['your_site']['password_secure'];
}elseif(isset($_SESSION['username'])){
@$username = $_SESSION['username'];
@$password_secure = $_SESSION['password_secure'];
}else{
return false;
}
include_once('./class/mysql.php');
$db = new mysqlConnection();
$db->change('whatever_database_you_use');
$db->query(sprintf("select password from users where username='%s'",
mysql_real_escape_string($username)));
if($db->numrows == 0){
return false;
}
$password_database = $db->result();
$password_secure_compare = md5(md5($password_database).$secure_global_pass.$username);
/* We're comparing another generated hash with the one supplied to us. If they
* don't match, the user is spoofing something, and shouldn't be allowed to see
* this page */
if($password_secure_compare <> $password_secure){
return false;
}else{
/* Since somebody might be viewing this page after saving the information to a cookie,
* we should log them in. */
/* Place the log-in in the session. */
$_SESSION['username'] = $username;
$_SESSION['pass_secure'] = $password_secure;
}

return $_SESSION['username'];
}

/* And here's the routine that's called on every page: */
$current_user = username();
if($current_user === false){
/* However you prefer to do this is fine */
page_forward('./login.php');
exit;
}


So, if the user isn't logged in correctly, they are forwarded immediately to the login page... Now, to get rid of the old method took replacing over 2000 references to $_SESSION['username'] for a username with $current_user. Since some of the $_SESSION['username'] instances were actually needed to modify the session, I had check each instance before I replaced it. Too much work.

Hope you get some ideas from that!

Comments: Post a Comment



<< Home

Archives

August 2005   September 2005   October 2005   November 2005   December 2005   January 2006   February 2006   March 2006   April 2006   June 2006   July 2006   August 2006   September 2006   October 2006   November 2006  

This page is powered by Blogger. Isn't yours?