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.
-
Are you posting in the right place? Check out Where do I post X? to know for sure.
-
Posts may use any of the Perl Monks Approved HTML tags. Currently these include the following:
<code> <a> <b> <big>
<blockquote> <br /> <dd>
<dl> <dt> <em> <font>
<h1> <h2> <h3> <h4>
<h5> <h6> <hr /> <i>
<li> <nbsp> <ol> <p>
<small> <strike> <strong>
<sub> <sup> <table>
<td> <th> <tr> <tt>
<u> <ul>
-
Snippets of code should be wrapped in
<code> tags not
<pre> tags. In fact, <pre>
tags should generally be avoided. If they must
be used, extreme care should be
taken to ensure that their contents do not
have long lines (<70 chars), in order to prevent
horizontal scrolling (and possible janitor
intervention).
-
Want more info? How to link
or How to display code and escape characters
are good places to start.