I made an admin login script for my own CMS website that I'm making in Code Igniter 2.2.1 and I'd like to know what are the potential threats or some holes that I might have left open.
This is how it goes:
- user submits form to admin_login page where the script collects the post data
- the script checks if the admin username exists in the database if it does then it runs
attemptLogin()
, if it doesn't then it kills the script attemptLogin()
method gets salt for the account- salts the password and encrypts it
- then it checks if the a row with salted password and admin username exists
- if it does it returns a unique session hash that it stores in in the database and adds expiration date to it
- two sessions are then created one contains the created session hash and the other one contains the username of the account
Then to check if the admin is logged on and to grant access to the Admin controller I do the following in the __construct
- check if admin username session is set
- if it's set then check if the session valid by checking if the session hash exists in the database
- if it doesn't exist we kill the script
- if the session hash exists in the database we take it's expiration date and compare it to the current time
- if it's expired we also kill the script
- if it exists and isn't expired then we do nothing therefore granting access to the controllers methods
Main Controller
This is basically the controller that works as a front page (took it out since it's not relevant) + admin login screen:
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Main extends CI_Controller {
public function __construct(){
parent::__construct();
$this->load->library("design");
session_start();
}
public function admin_login_form(){
$this->load->helper("form");
$this->design->loadEssentials();
$this->load->view("cols/admin-login-form-col-3");
$this->load->view('footer');
}
public function admin_login(){
//load the login model
$this->load->model("login_model", "login");
$username = $_POST['admin_username'];
$password = $_POST['admin_password'];
$http_forwarded_ip = getenv('HTTP_X_FORWARDED_FOR');
$ip = getenv('REMOTE_ADDR');
if($this->login->checkAdminExists($username, $ip, $http_forwarded_ip)){
//if function above returns 1 the the admin exists in the database
//and we attemt to log in using the username and password from
if($hash = $this->login->attemptLogin($username, $password, $ip, $http_forwarded_ip)){
//if attemt returns a hash we create a session with admin username and hash and
$_SESSION['admin_username'] = $username;
$_SESSION['session_hash'] = $hash;
}
}
}
}
Login model
This is where most of the functions reside dedicated to checking data and logging stuff into the database:
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Login_Model extends CI_Model{
private $timestamp;
public function __construct(){
parent::__construct();
$this->load->database();
$this->datetime = new DateTime();
}
public function checkAdminExists($admin_username, $ip, $http_forwarded_ip){
if(isset($admin_username)){
$timestamp = $this->datetime->getTimestamp();
// select admin username from database where it equals the username the user inputs
$this->db->select('admin_username');
$this->db->from('admin_info');
$this->db->where(array("admin_username" => $admin_username));
//count the results the query returns 1 means the admin exists 0 means admin doesn't exist
$result = $this->db->count_all_results();
if($result == 1){
return $result;
}else{
$this->_logAttempt(0, $timestamp, 1, $ip, $http_forwarded_ip, $admin_username);
die("wrong username");
}
}
}
public function attemptLogin($admin_username, $admin_password, $ip, $http_forwarded_ip){
if(isset($admin_username) && isset($admin_password)){
//get UNIX timestamp
$timestamp = $this->datetime->getTimestamp();
//get salt of the admins password
$salt = $this->_getSalt($admin_username);
//append salt to plaintext password
$salted_pass = $admin_password.$salt;
//hash the salted password
$hashed_pass = hash("sha256", $salted_pass);
//select row where admin username = admin_password and where admin password = hashed and salted password and then count results
$this->db->select("*");
$this->db->from("admin_info");
$this->db->where(array("admin_username" => $admin_username, "admin_password" => $hashed_pass));
$sql = $this->db->count_all_results();
//the query should return either 1 if an account with such username and salted+hashed password combination exists
//or 0 if it doesn't
if($sql == 1){
//if an account exists then we log the attemt as successful
$this->_logAttempt(1, $timestamp, 0, $ip, $http_forwarded_ip);
/*generate a hash that we are gonna store in the database and a session to check if the session has expired
*and also make it neccessary to validate the username session so if someone finds a way to create an admin session
*it will be useless if they don't have a hash stored in a session and database
*/
$hash = bin2hex(openssl_random_pseudo_bytes("16"));
$this->_logSession($hash, $timestamp);
return $hash;
}else{
//if account doesn't exist we log the attempt as unsuccessful ad kill the script
$this->_logAttempt(0, $timestamp, 0, $ip, $http_forwarded_ip, $admin_username." ".$admin_password);
die("wrong username or password");
}
}
}
private function _logAttempt($outcome, $timestamp, $stage, $ip, $http_forwarded_ip, $inputs = 0){
/*outcome is either 0 if failed or 1 if successful
*if outcome failed stage represents the part of the login at which the user failed to log in at
*stage = 0 if login was successful
*checkAdminExists() is stage 1 and attemptLogin() is stage 2
*inputs shows what username and password(unhashed) the user was trying to input if outcome failed
*/
$data = array(
'attempt_outcome' => $outcome,
'attempt_stage' => $stage,
'attempt_timestamp' => $timestamp,
'attempt_ip' => $ip,
'attempt_forwarded_ip' => $http_forwarded_ip,
'attempt_inputs' => $inputs
);
$this->db->insert('login_attempts', $data);
}
private function _getSalt($admin_username){
if(isset($admin_username)){
$this->db->select("admin_password_salt");
$this->db->from("admin_info");
$this->db->where(array("admin_username" => $admin_username));
$sql = $this->db->get();
$sql = $sql->result();
$salt = $sql[0]->admin_password_salt;
return $salt;
}
}
private function _logSession($hash, $timestamp){
//insert the newly created session hash and add to hours to current time as session expiration time
$data = array(
'session_hash' => $hash,
'session_expire' => $timestamp + 7200
);
$this->db->insert('session_log', $data);
}
public function checkSessionExpire($hash){
//get UNIX timestamp
$timestamp = $this->datetime->getTimestamp();
//select expiration time from hash row where session_hash = $hash
$this->db->select('session_expire');
$this->db->from('session_log');
$this->db->where(array("session_hash" => $hash));
$result = $this->db->get()->result();
$expire_date = $result[0]->session_expire;
if($timestamp > $expire_date){
//if current time is greater(therefore older) than expiration return false
return false;
}else{
//otherwise the session hasn't expired and return true
return true;
}
}
}
This is the table where the session hash is stored:
+----------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------------+--------------+------+-----+---------+----------------+ | session_id | int(11) | NO | PRI | NULL | auto_increment | | session_hash | varchar(225) | NO | | NULL | | | session_expire | bigint(20) | NO | | NULL | | +----------------+--------------+------+-----+---------+----------------+
This is the table for admin data:
+---------------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------------------+--------------+------+-----+---------+----------------+ | admin_id | int(11) | NO | PRI | NULL | auto_increment | | admin_username | varchar(32) | NO | | NULL | | | admin_password | varchar(255) | NO | | NULL | | | admin_password_salt | varchar(255) | NO | | NULL | | | admin_email | varchar(255) | NO | | NULL | | +---------------------+--------------+------+-----+---------+----------------+
Admin controller
All of the relevant code exists in the controller:
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Admin extends CI_Controller{
public function __construct(){
parent::__construct();
$this->load->model('login_model', "login");
session_start();
if(isset($_SESSION['admin_username'])){
//if username session is set we check if there is a valid session hash set
$valid = $this->login->checkSessionExpire($_SESSION['session_hash']);
if(!$valid){
/*if the hash is expired or if it's not in the database at all unset both admin_username session and session_hash,
*kill the script and send user to fornt page
*/
unset($_SESSION['admin_username']);
unset($_SESSION['session_hash']);
die(header("Location:".base_url()));
}
}
else{
//if admin username isn't set at all kill the script and send user to front page
die(header("Location:".base_url()));
}
}
}