5
\$\begingroup\$

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()));
        }
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ What happens if some jolly joker copy/pastes in a million character password? I suspect your server might grind to a halt as it tries to hash it. \$\endgroup\$
    – Cerad
    Commented Jan 28, 2015 at 22:51
  • \$\begingroup\$ PHP has this function you should use php.net/manual/en/function.password-hash.php \$\endgroup\$
    – CodeX
    Commented May 25, 2015 at 10:31

1 Answer 1

1
\$\begingroup\$

First off, if this is for your CMS website, this could potentially be a little overkill. If it's just you (maybe one or two others), an XML/JSON file with hashed credentials could work just as well as a relational database. The database works well if you've a hundred, or a thousand, or even a million users. It doesn't seem like you need all this capacity. On the other hand, you might need this type of capacity, which then I would suggest one of the many NoSQL systems.

However, here's some comments on what you do have:

Security

  • As @CodeX suggested, use password_hash(). It will eliminate much of the anxiety over having "safe enough encryption." Plus it will reduce the amount of code needed.
  • In admin_login(), it seems superfluous to check the username exists and then check for authentication. Dot it all in one step instead of having two check against the database twice.
  • For more detail on using sessions and maintaining security, check out the PHP documentation.

Code

  • Avoid using die in production code. It's not user friendly and can lead to more errors than it might be protecting against.
  • Consider using try...catch statements instead of having many if conditions. Throwing errors will allow you to articulate the error in a more scalable and debuggable way.
  • Getting the user's IP is tricky stuff, and the first few links on Google are a little dated. Here's a blog post with a snippet that should be a little more up to date.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.