CodeWatch

This week we will cover the authentication portion of OWASP A3. I’m not following any particular order, just going in the direction I feel like. So if it seems out of order, it probably is. This section will begin covering ASVS 2.x.

ASVS 2.1 Requirement:

Verify that all pages and resources require authentication except those specifically intended to be public.

ASVS 2.1 Solution:

This solution is fairly simple. It is as easy as implementing a header file that you require on all pages that should require authentication. This header file should use the session_start(); function and validate that session variables have been set for the user, such as $_SESSION[‘user’] or whatever you name your username session variable. An example:

  // Resume the session so that it can be deleted
  session_start();

  // I like to make sure the session cannot be cached by either the client or a proxy.
  // This can be accomplished with the following:
  session_cache_limiter('nocache');

  // Validate that the user and pass session variables have been set.
  // If not, the session is not valid and should be destroyed, and the
  // user should be redirected to the login page.  Check to see if the 
  // session variable used to track the login time has been set,
  // if not then destroy all session data and redirect to the login page. If it has
  // been set, compare current time minus the last activity time, and if it is
  // greater than 900 seconds (15 minutes), then destroy session data and
  // redirect to the login page.  Validate that the IP address for the client
  // hasn't changed, otherwise redirect to the login page.
  if (!isset($_SESSION['user']) || !isset($_SESSION['token']) || !isset($_SESSION['IPAddr']) ||     
        !isset($_SESSION['Last_Activity']) || ((time() - $_SESSION['Last_Activity']) > 900) || 
        ($_SESSION['IPAddr'] != $_SERVER['REMOTE_ADDR']) || ) {
    session_destroy();
    $_SESSION = array();

    // Delete cookie data
    if (ini_get("session.use_cookies")) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000,
            $params["path"], $params["domain"],
            $params["secure"], $params["httponly"]
        );
    }

    // Regenerate the session ID
    session_regenerate_id(true);
    header('Location: login.php');
    die();
  } else {
    // If the session variable for tracking activity has been set AND we have seen
    // activity within the last 15 minutes, then update the last activity timer with
    // the current time.
    $_SESSION['Last_Activity'] = time();
  }

 
ASVS 2.2 Requirement:

Verify that all password fields do not echo the user’s password when it is entered, and that password fields (or the forms that contain them) have autocomplete disabled.

ASVS 2.2 Solution:

Another pretty simple solution to implement in code. This requires HTML code in the form fields. To prevent echoing the user’s password, set the type to password:

  <input name='userpass' type='password'>

 
Adding the autocomplete=off attribute to the field value will prevent the browser from storing the password:

  <input name='userpass' type='password' autocomplete='off'>

 
Or, you can disable autocomplete for the entire form:

  <form action='login.php' method='POST' autcomplete='off'>
    <input name='username' type='text'>
    <input name='userpass' type='password'>
  </form>

 
ASVS 2.3 Requirement:

Verify that if a maximum number of authentication attempts is exceeded, the account is locked for a period of time long enough to deter brute force attacks.

ASVS 2.3 Solution:

This is a little more difficult from a code and complexity standpoint. This can be implemented with a database backend that tracks account login failures as well as an account lockout timer. In this example we are assuming that the account lockout threshold is 5, the account lockout time is 30 minutes, and that you have a database with a table named customerTbl. The customerTbl contains at least the following columns: Username, Userpass, LockoutTotal, LockoutTime. The steps include:

  1. Validate that the account exists.
  2. Validate whether or not the correct username and password were provided.
  3. If an incorrect username/password combination was provided, check to see if the lockout counter has been exceeded. If it was just exceeded, update the account lockout threshold for the current value and set the account lockout timer to current time + 30 minutes. Reject the authentication request.
  4. If the correct username/password was provided, and the account lockout threshold hasn’t been exceeded or the account lockout timer has expired, then authenticate and clear the account lockout values.

Some example (not necessarily efficient) code:

  // Query the database to return data from a match on user name 
  // and password (authentication)
  $customer_auth_qry = $db->prepare(
      "SELECT LockoutTotal, LockoutTime FROM customerTbl WHERE Username = ? AND Userpass = ?");
  $customer_auth_qry->bindValue(1, $username, PDO::PARAM_STR);
  $customer_auth_qry->bindValue(2, $userpass, PDO::PARAM_STR);
  $customer_auth_qry->execute();
  $customer_auth_rslt = $customer_auth_qry->fetch();

  // Determine whether there is even a match on the user name, if not 
  // then they can't auth
  $account_qry = $db->prepare("SELECT Username FROM customerTbl WHERE Username = ?");
  $account_qry->bindValue(1, $username, PDO::PARAM_STR);
  $account_qry->execute();
  $account_rslt = $account_qry->fetch();

  // If there was an error in the authentication lookup then they can't login
  if ($customer_auth_qry->errorcode() > 0) {
    return "Invalid Username and/or Password";
  } else {
    // If a valid user was used then they might be able to authenticate, 
    // else auth fails
    if (sizeof($account_rslt) > 1 ) {
    
      // If user name and password matches then authentication is a success
      // but we must also check whether the account is locked out
      if (sizeof($customer_auth_rslt) > 1) {
        
        // Get the lockout period that was set due to a previous lockout or 
        // get the number of failed login attempts
        $lockout_period = $customer_auth_rslt['LockoutTime'];
        $lockout_total= $customer_auth_rslt['LockoutTotal'];
      
        // Call a custom function to get the current time
        $current_time = currentDateTime();

        // If the account is under the lockout total or has passed the lockout 
        // timeframe then login and reset lockout counters
        if ($customer_auth_rslt['LockoutTotal'] < 4 || $lockout_period < $current_time) {
          $customer_upd_qry = "UPDATE customerTbl SET LockoutTotal = 0, 
              LockoutTime = '0000-00-00 00:00:00' WHERE Username = ? AND Userpass = ?";
          $customer_upd_exec = $db->prepare($customer_upd_qry);
          $customer_upd_exec->execute(array($username, $password));
                                    
          return $customer_auth_rslt;
        } else {
          // The account lockout total has been exceeded and the lockout 
          // period has not expired yet.  Do not allow authentication until
          // the lockout period expires.
          return "Invalid Username and/or Password";
        }
      } else {
        // Username and password combination didn't match, so we must 
        // increment the lockout total and input the lockout time (in case 
        // we have hit the lockout threshold)
        $lockout_period = date('Y-m-d H:i:s', strtotime("+" . 30 . " Minutes"));
        $lockout_total= ($customer_auth_rslt['LockoutTotal']+1);

        // Authentication failed and therefore increment number of failed 
        // attempts and add 30 minutes to current time
        $customer_upd_qry = "UPDATE customerTbl SET LockoutTotal = ?, 
            LockoutTime = ? WHERE Username = ?";
        $customer_upd_exc = $db->prepare($customer_upd_qry);
        $customer_upd_exc->execute(array($lockout_total, $lockout_period, $username));

        return "Invalid Username and/or Password";
      }
  } else {
    // The database query for the auth check failed, therefore the 
    // login cannot continue
    return "Invalid Username and/or Password" ;
  }

 
ASVS 2.4 Requirement:

Verify that all authentication controls are enforced on the server side.

ASVS 2.4 Solution:

Don’t perform authentication on the client side. Use code similar to that above on the server side to validate authentication.

ASVS 2.5 Requirement:

Verify that all authentication controls (including libraries that call external authentication services) have a centralized implementation.

ASVS 2.5 Solution:

My interpretation of this control is that all controls related to authentication should be implemented within the same service/server. All of the above code would reside on the service/server providing authentication to the application.

ASVS 2.6 Requirement:

Verify that all authentication controls fail securely.

ASVS 2.6 Solution:

If an error occurs in the database lookup or in any other scenario, authentication should fail. Ensure the application always fails closed in the event of an error or invalid data.

There is quite a bit of code in a few of the provided examples so I will stop here and pick up where we left off next week. Don’t worry, if you are scratching your head at the database code, we will explain when we get to SQL injection (part of OWASP A1).

Leave a Reply

Your email address will not be published. Required fields are marked *