-
OWASP A3 – Broken Authentication and Session Management Defenses with PHP Part 4
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:
- Validate that the account exists.
- Validate whether or not the correct username and password were provided.
- 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.
- 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 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).
-
OWASP A3 – Broken Authentication and Session Management Defenses with PHP Part 3
I took last week off due to the holiday and what not. So this week we will pick back up where we left off, and hopefully continue adding a post a week. Two weeks ago we stopped at ASVS 3.9, this week we will start with ASVS 3.10 and finish up ASVS section 3.
ASVS 3.10 Requirement:
Verify that only session ids generated by the application framework are recognized as valid by the application.
ASVS 3.10 Solution:
Utilize the session functionality built within PHP; this should prevent an attacker from using a session ID that was not generated by PHP. To prevent an attacker from guessing a valid session ID, use the strong session security features of PHP. Use a strong hash function such as sha512 with this php.ini configuration:
session.hash_function = 'sha512'
Use the widest range of bits when representing the binary session ID in a readable form:session.hash_bits_per_character = 6
Use a strong source of randomness to create the session ID:session.entropy_file = /dev/urandomRead more about these options here. Learn how to identify the hashing algorithms available for your platform here.
For additional security, you can prevent session fixation attacks (using a previously valid session ID and setting it for a victim via a phishing attack or something similar) with a number of countermeasures.
- First, make sure that sessions are only transmitted in cookies, and never by URLs by reviewing the ASVS 3.6 solution in our last post.
- Next, make sure that session IDs are changed after authentication and destroyed after logout. The solution for this is found in our last post covering ASVS 3.7, 3.8, and 3.9.
- Finally, do not rely on the fact that a session exists by only calling session_start(). Make sure that valid session values have been set (these should be set with $_SESSION[‘user’] = variable in your authentication code):
// 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. if (!isset($_SESSION['user']) || !isset($_SESSION['pass'])) { session_destroy(); $_SESSION = array(); if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"] ); } session_regenerate_id(true); header('Location: login.php'); die(); }
ASVS 3.11 Requirement:Verify that authenticated session tokens are sufficiently long and random to withstand attacks that are typical of the threats in the deployed environment.
ASVS 3.11 Solution:
See the information above that describes how to configure PHP to use a strong algorithm and source of randomness.
ASVS 3.12 Requirement:
Verify that cookies which contain authenticated session tokens/ids have their domain and path set to an appropriately restrictive value for that site.
ASVS 3.12 Solution:
This will limit where the cookie can be utilized. Cookie paths can be set globally in php.ini with the following directives:
// Set the cookie path session.cookie_path = /path/that/cookie/applies // Set the domain path session.cookie_domain = www.mysite.com
This can also be set at the application level with a call to session_set_cookie_params():// Set the lifetime of the cookie to 15 minutes, set the path to // /securesite/admin, set the domain to www.mysite.com, then // set the secure and httponly flags (discussed later). session_set_cookie_params( 900, '/securesite/admin', 'www.mysite.com', true, true );
ASVS 3.13 Requirement:Verify that all code implementing or using session management controls is not affected by any malicious code.
ASVS 3.13 Solution:
Scan your code for viruses, or any malicious backdoors. Perform manual and automated code reviews.
Bonus!:
Two other requirements are found in the OWASP ASVS that I believe are relevant to protecting sessions. These are found in V11 – HTTP Security Verification Requirements. The specific requirements are ASVS 11.4 and 11.5.
ASVS 11.4 & 11.5 Requirement:
Verify that the HTTPOnly flag is used on all cookies that do not specifically require access from JavaScript.
Verify that the secure flag is used on all cookies that contain sensitive data, including the session cookie.
ASVS 11.4 & 11.5 Solution:
These values help protect sensitive cookie data. The HTTPOnly flag prevents the cookie from being accessed by client side code (such as JavaScript), if the client browser supports the flag. This mechanism helps protect the cookie in the event that a cross-site scripting (XSS) flaw exists in the application. The secure flag prevents the cookie from being sent over a clear-text (non-encrypted) HTTP connection.
These flags can be set in the php.ini file globally:
// Set the httponly flag session.cookie_httponly = true // Set the secure flag session.cookie_secure = true
Alternatively, this can be set in the application code with a call to session_set_cookie_params() as seen in the solution to ASVS 3.12.Next week we will begin covering authentication controls that most applications should implement.
-
OWASP A3 – Broken Authentication and Session Management Defenses with PHP Part 2
We covered OWASP ASVS 3.1, 3.2, and 3.3 in our previous post. I will continue where we left off, beginning this week’s post with ASVS 3.4.
ASVS 3.4 Requirement:
Verify that sessions timeout after an administratively-configurable maximum time period regardless of activity (an absolute timeout). I am not sure that this is a security requirement for most applications, but the intent here is to make a user re-authenticate after a certain period of time regardless of activity.
ASVS 3.4 Solution:
Review my first post for a similar concept. You would need to create a session variable that is initialized at each login with something like this:
$_SESSION['Login_Time'] = time();
Then, you need to compare the current time with the initial login time. This could be validated in a header file.The steps in PHP are:
- Check to see if the session variable used to track activity has been set. If not, destroy any session data and redirect to the login page.
- Check to see if the current time minus the time recorded for the initial login is greater than 86,400 seconds (24 hours). If the timer has been exceeded, then destroy any session data and redirect to the login page.
The following code can be placed in a header file that is included by every 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 86,400 seconds (24 hours), then destroy session data and // redirect to the login page. if (!isset($_SESSION['Login_Time']) || ((time() - $_SESSION['Login_Time']) > 86400)) { session_destroy(); $_SESSION = array(); if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"] ); } header('Location: login.php'); die(); }
This could be AJAX-ified similar to what we did last week.ASVS 3.5 Requirement:
Verify that all pages that require authentication to access them have logout links.
ASVS 3.5 Solution:
Another easy fix requiring little code; add a link to the logout page on each page for which the user must be authenticated. This enables users to easily logout of their session and thus destroys associated session data.
ASVS 3.6 Requirement:
Verify that the session id is never disclosed other than in cookie headers; particularly in URLs, error messages, or logs. This includes verifying that the application does not support URL rewriting of session cookies.
ASVS 3.6 Solution:
This requirement reduces the likeliness that an attacker can gain access to session ID’s. Part of this requirement can be satisfied with the right PHP configuration. Configure your php.ini file with the following settings:
// PHP will use cookies for maintaining session state session.use_cookies = 1 // PHP will only use cookies to store session ID's on the client side session.use_only_cookies = 1 // Disable PHP's support for URL based sessions session.use_trans_sid = 0
If you must include a session ID in form data for some reason, use the HTTP POST method, so that it will not be logged on the server side (GET requests are logged).ASVS 3.7 & 3.8 Requirements:
Verify that the session id is changed on login.
Verify that the session id is changed on reauthentication.
ASVS 3.7 & 3.8 Solution:
This requirement helps prevent session fixation attacks. If an attacker were to trick a user into visiting the application and the session ID is part of the URL, then changing the session ID upon login will prevent a successful session fixation attack. Add the following snippet of source after the code on your login page in which a user’s authentication is validated:
// Generate a new session ID and assign it to the current session session_regenerate_id(true);
ASVS 3.9 Requirement:Verify that the session id is changed or cleared on logout.
ASVS 3.9 Solution:
Add the code above for ASVS 3.7 and 3.8 to the logout page:
session_regenerate_id(true);
This code could come before the redirect to the login page found in our previous post for ASVS 3.2.Next week we should finish up ASVS section 3.x. We will then cover the authentication requirements in ASVS section 2.x to wrap up our discussions on OWASP A3 – Broken Authentication and Session Management.
-
Inaugural Post: OWASP A3 – Broken Authentication and Session Management Defenses with PHP Part 1
I have finally gotten around to starting a blog and this will be my first post in what is hopefully a long running series of useful secure development posts. Given that I am in the business of application security, specifically web applications, I have decided to do a series on OWASP protections. I will try and do a series for several major languages (perl, PHP, Java, ASP.NET), starting off with PHP. I am going to randomly select OWASP Top 10 of 2010 risks and provide countermeasures in the current language for the series. I am going to try to cover all of the verification requirements listed in the OWASP Application Security Verification Standard (ASVS) Project.
I know that this has probably been done before somewhere. I just wanted to create a central repository for myself and others on developing secure applications. I will probably learn just as much as anyone else reading this blog along the way.
I’m not perfect and will certainly make mistakes; either with inefficient code or just plain bad code. Please let me know if you see something that is wrong, off, or could be done a little better.
So without further ado…
As the title suggests, this post will cover part of OWASP A3 – Broken Authentication and Session Management (2010). There are 13 ASVS validation requirements, so I am going to break this up into several posts.
ASVS 3.1 Requirement:
Verify that the framework’s default session management control implementation is used by the application.
ASVS 3.1 Solution:
This is a fairly obvious one. If you are using PHP, then utilize the native PHP session management functionality in your application. Do not roll your own session management unless it is absolutely necessary (which is usually not the case). The default or native session management functionality in common systems is usually more secure.
ASVS 3.2 Requirement:
Verify that sessions are invalidated when the user logs out.
ASVS 3.2 Solution:
Ensure that all session data is deleted if a user logs out, such that no one can use the associated session ID’s or cookies to gain access to the application. If the session isn’t properly invalidated, then a malicious individual might be able to obtain authenticated access to your application under another user’s account via a session ID.
The steps in PHP are:
- Resume the current session so that it can be destroyed.
- Destroy the current session.
- Unset the session ID (destroying the session does not unset global variables).
- Delete associated cookies (destroying the session does not delete cookie data).
- Redirect to the login page.
The following code can be placed in a logout.php file that is called when the user logs out:
<?php // Resume the session so that it can be deleted session_start(); // Destroy the current session session_destroy(); // 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'); // Unset all session variables, including the session ID $_SESSION = array(); // Kill the session by deleting the session cookie. This will destroy the session // and session 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"] ); } // Redirect to the login page: login.php header('Location: login.php'); // Stop all further processing on the page die(); ?>
ASVS 3.3 Requirement:Verify that sessions timeout after a specified period of inactivity.
ASVS 3.3 Solution:
Ensure that sessions timeout if a user steps away from the application for an extended period of time.
The steps in PHP are:
- Resume the current session so that it can be destroyed.
- Check to see if the session variable used to track activity has been set. If not, destroy any session data and redirect to the login page.
- Check to see if the current time minus the time recorded in the last activity is greater than 900 seconds (15 minutes). If the inactivity timer has been exceeded, then destroy any session data and redirect to the login page.
- If the activity tracking session variable is set and the 15 minute timer has not elapsed then update the variable to the most recent activity time.
The following code can be placed in a header file that is included by every page:
<?php session_start(); session_cache_limiter('nocache'); // Check to see if the session variable used to track user activity 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. if (!isset($_SESSION['Last_Activity']) || ((time() - $_SESSION['Last_Activity']) > 900)) { session_destroy(); $_SESSION = array(); if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"] ); } 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(); } ?>
Bonus:I like to create a checkSession.php file with the above code, with the following exceptions:
- Remove the else statement.
- Replace the header function with the following:
print("login.php");
I then create a small bit of AJAX to call this page in the background every minute. If the session goes stale then it will automatically logout, destroy session data, and redirect the user to the login page. Otherwise, nothing will happen until a user does something in the application (I hope that make sense).The AJAX code:
<script> // Create a javascript variable to track the 60 second interval checks var interval // Create the AJAX function that will be called every 60 seconds to validate the user's session function checkSession() { var http; // Standard AJAX setup code, beyond the scope of this post if (window.XMLHttpRequest) { http = new XMLHttpRequest(); } else if (window.ActiveXObject) { http = new ActiveXObject(\"Msxml.XMLHTTP\"); if (! http) { http = new ActiveXObject(\"Microsoft.XMLHTTP\"); } } // When the function is called, check the return data of the checkSession.php code, if the data // contains login.php (from the print statement), then redirect to the login.php page http.onreadystatechange = function() { if (http.readyState == 4) { if (http.responseText == 'login.php') { window.location.href=http.responseText; } } } // When the function is called, make a request to checkSession.php http.open(\"GET\", \"checkSession.php\", true); http.send(null); } // On loading this page, set the interval variable. Run the function every 60 seconds. window.onload = function() { interval = setInterval('checkSession()', 60*1000);// 60 secs between requests }; </script>
I think that about does it for this week. Next week I will pick up where we left off. Questions, comments, and feedback are always welcome. Until next week…