Ovid has asked for the wisdom of the Perl Monks concerning the following question:
Looking for feedback on the security model that I have for a particular site. We've been asked to encrypt only the login process (SSL). This site is for an administration console and will only have a few users. They are required to have cookies enabled. Here's basically how it works:
The user enters a username/password combination. This is sent over an ssl connection to the authentication script. The password is hashed using Digest::MD5 and a salt that is read from a non-web accessible file. This is compared to the hashed password in the database. If they don't match, they are redirected to the login screen up to seven times, after which they are locked out until an administrator unlocks them. If successful, a digest is created with the following algorithm:
sub _create_session_digest { # Please note that we will compare the digest against what's in th +e database rather than # recompute. It's quite possible for someone's ip address to chan +ge with every request. my $self = shift; my $md5 = new Digest::MD5; my $remote = $ENV{ REMOTE_ADDR } . $ENV{ REMOTE_PORT } . $self->{ +_salt }; my $id = $md5->md5_base64( time, $$, $remote ); $id =~ tr|+/=|-_.|; # Make non-word characters URL-friendly $id; }
This digest is returned as a cookie. Subsequent accesses to the admin console will return the digest and compare it to the database. If this takes too long, their session times out (controlled by the script, not the cookie) and they must relogin. If the digest matches, a new digest is created, stored in the database, and sent back in a cookie. Except for the initial SSL connection when the username and password are submitted, they will never again be sent. Here's the main code that controls this:
# Everything in ALL CAPS is a constant sub validate_and_get_new_cookie { my ( $self, $cgi, $user, $pass ) = @_; my $cookie = $cgi->cookie( SESSION_COOKIE_NAME ); # delete sessions older than that session ID's allowed timeout $self->_clear_old_session( $cookie ); if ( defined $user and defined $pass ) { # they're submitting a username and password, so let's try to +log them in my $attempts = $self->_count_login_attempts( $user ); $self->_lockout if $attempts >= MAX_LOCKOUT_ATTEMPTS; my $db_pass = $self->_get_password( $user ); my $user_pass = $self->_create_digest_from_password( $pass ); if ( $db_pass eq $user_pass ) { return $self->_create_digest_cookie( $user ); } else { my $attempts = $self->_update_attempts( $user ); $self->_log_bad_attempt( $user, $pass ); $self->_lockout if $attempts >= MAX_LOCKOUT_ATTEMPTS; print $q->redirect( LOGIN_PAGE ); } } else { # no user or password, so we'll try to validate with the cooki +e my ( $user, $active ) = $self->_get_digest_info( $cookie ); if ( ! defined $user or ! $active ) { # didn't get a user name or they've been inactive too long print $q->redirect( LOGIN_PAGE ); } else { return $self->_create_digest_cookie( $user ); } } }
In the future, I plan to add a 'bogusLogin' table to the database. The intent is to even lockout non-existent user IDs after MAX_LOCKOUT_ATTEMPTS so that crackers can't use the lockout feature to determine if they have a valid user id. Have I overlooked anything?
Cheers,
Ovid
Vote for paco!
Join the Perlmonks Setiathome Group or just click on the the link and check out our stats.