How to avoid Identity Theft in Zend Framework with Zend Auth

March 5, 2010

Zend Framework

As I am building my applications, I always try to improve the code I write in some way. Today I thought about the security issues of any PHP application that uses an authenticating system.

Web application security issues

The major issues with security within web application are the following:

  • Cross-site Scripting (see Wikipedia – Cross-site scripting )
  • Unvalidated parameters
  • Broken access control
  • Error-handling problems
  • Insecure use of cryptography
  • Web and application server misconfiguration
  • Broken account and session management

While all are major issues, there is one particular issue that bugged me for some time. The Identity theft – Broken account and session management issue.

Why can one so easily still my session id cookie and suddenly gain access to my account in one particular web application? I know it its rather impossible to make this 100% hack-proof but I strongly believe that the system should be improved as much as possible.

In the following few lines, I will show you how you can just do that.

Our goal

Our goal is to implement a Zend Auth extension that adds a new level of security to the previously mentioned class.

This extension – let’s call it Project_Application_Auth – would check the Zend Auth storage for the IP and/or User Agent.

In order to do so, these should be set in the login process in the storage.

If the IP is different then the initial IP from the login process and / or the User Agent is not the same as the initial User Agent from the login process, then our extension would tell us that it is not a secure identity (aka it is safe to assume it has been stolen) and thus we should disconnect the user.

Reasons to use these methods

I came up with this idea since I asked myself: Is there really a case where i would actually end up with the same session ID but different IP or different browser?

The answer is: yes. There is a chance with the first-one-mentioned. If I have a dynamic IP, this could change without me knowing, which would result in me being unauthenticated.

But the fact is, this happens rarely (even if you IP changes, it won’t change more then once per week, right?).

You might ask why should the extension that I am about to show you, check the IP also when we can just check the User Agent (since this definitely cannot be the same on a different IP since the session id cookie would exist in only one browser, not any other ).

The answer is: because any hacker that stole your identity might just use the same browser as you did and thus bypass our checks completely.

And as a final reason, yes, if the hacker has the same IP as yours, steals your identity (session cookie id) and uses the same browser, then he will bypass the system. But hey, at least we can make it harder for him!

The Project Application Auth extension

The following class should go into your /path/to/the/project/……./library/Project/Application/Auth.php

class Project_Application_Auth extends Zend_Auth
{
    /**
     * Singleton instance
     *
     * @var Project_Application_Auth
     */
    protected static $_instance = null;
    
    
    /**
     * Defines how much time to wait until 
     * to reinitialize the session id
     */
    protected static $_session_exp_time = 5;
    
    /**
     * Defines if to validate a secure identity
     */
    protected static $_secure = TRUE;
    
    /**
     * Defines secure identity check level
     * 
     * 1 - Check only IP
     * 2 - Check only UserAgent
     * 3 - Check IP & UserAgent
     */
    protected static $_secure_level = 3;
    
    /**
     * Returns an instance of Project_Application_Auth
     *
     * Singleton pattern implementation
     *
     * @return Project_Application_Auth Provides a fluent interface
     */
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }
    
    /**
     * Sets wheter the method @see hasSecureIdentity 
     * to work or not. If set to FALSE then this extension will work
     * as the normal Zend_Auth class 
     * 
     * @param boolean true
     */
    public function setSecure($status = TRUE)
    {
    	if ($status === TRUE)
    	{
    		self::$_secure = TRUE;
    	}
    	
    	if ($status === FALSE)
    	{
    		self::$_secure = FALSE;
    	}
    	
    	return TRUE;
    }
    
    /**
     * Sets the level of security for the @see hasSecureIdentity method
     * 
     * @param integer $level (can be 1 or 2 or 3)
     */
	public function setSecureLevel($level)
    {
    	if (in_array($level, array(1, 2, 3)))
    	{
    		self::$_secure_level = $level;
    	}
    	
    	return TRUE;
    }
	
    /**	
     * Returns true if and only if an identity is not stolen
     * 
     * Checks if IP and/or User Agent (@see $_secure_level) 
     * match the initial authentication data 
     * 
     * @return boolean
     */
    public function hasSecureIdentity()
    {
    	if (self::$_secure === FALSE)
    	{
    		return TRUE;
    	}
    	
    	if (FALSE == $this->getStorage()->isEmpty())
    	{
	    	$storage = $this->getStorage()->read();
	    	
	    	if (self::$_secure_level == 3)
	    	{
		    	return $storage->ip == $_SERVER['REMOTE_ADDR'] 
		    		&& $storage->user_agent == $_SERVER['HTTP_USER_AGENT'];
	    	}
	    	elseif (self::$_secure_level == 2)
	    	{
	    		return $storage->user_agent == $_SERVER['HTTP_USER_AGENT']; 
	    	}
	    	elseif (self::$_secure_level == 1)
	    	{
	    		return $storage->ip == $_SERVER['REMOTE_ADDR']; 
	    	}
	    	else
	    	{
	    		return FALSE;
	    	}
    	}
    	else
    	{
    		return FALSE;
    	}
    }
    
    /**
     * If the Zend Auth Storage 
     * has been initialized and is a Session Storage
     * and the last time it has been reinitialized is bigger then
     * the @see $session_exp_time then reinitialize the session id  
     * 
     * @return boolean
     */
    public function reinitSecurity()
    {
    	if (isset($_SESSION['Zend_Auth']))
    	{
	   		$zend_auth_session_namespace = new Zend_Session_Namespace('Zend_Auth');
			if (!isset($zend_auth_session_namespace->initialized)
				|| $zend_auth_session_namespace->initialized + self::$session_exp_time < time() ) {
				Zend_Session::regenerateId();
				$zend_auth_session_namespace->initialized = time();
			}
    	}
    	
    	return TRUE;
    }
}

Now, let’s stop for a second and understand what all these spooky lines do:
First of all, the extension implements the singleton pattern. Then, we got 3 properties:

The first one -is_secure – allows us to turn on and off the whole class, leaving us with the usual Zend Auth system.

The second one, secure level, defines what to check for: either just IP or just User Agent or both.

The third one, session_exp_time defines when to reinitialize the Zend Auth Session Storage Id (aka PHPSESSID id) if the method reinitSecurity is called.

How to use the class

In your base controller – let’s call it Project_Application_Controller (which all controllers should extend) – you should have the following code :


abstract Class Project_Application_Controller extends Zend_Controller_Action
    /**
     * Initialize the application controller
     */
	public function init() 
	{
		parent::init();
		$this->_initSession();
		debug($_SESSION);
	}
	
	/**
	 * Inits the User Session
	 * and refresh the session id
	 */
	protected function _initSession()
	{
		Project_Application_Auth::getInstance()->reinitSecurity();
	}

This basically just allows us to run the method reinitSecurity with each run of the application, while still allowing us to initialize the whole Front Controller.

Now, in your preDispatch() method you might have something like this:


if ( FALSE === Project_Application_Auth::getInstance()->hasIdentity() { //do stuff here } else { //other stuff here }

This basically just checks if the user has been authenticated previously. Exactly underneath this, add the following code:


if ( FALSE === Project_Application_Auth::getInstance()->hasSecureIdentity()
    && 'users' !== $this->getRequest()->getControllerName()
    && 'login' !== $this->getRequest()->getActionName()
    && 'error' !== $this->getRequest()->getControllerName())
{
    Zend_Auth::getInstance()->clearIdentity();
    // redirect to login page
}

I am assuming that you have a controller named users with an action login into it (change as you wish).

Lastly, in your in your login process, wherever you are running your Zend_Auth authenticate() method, add the following code :


$auth = Project_Application_Auth::getInstance();
		
$result = $auth->authenticate($authAdapter);

if ($result->isValid())
{
    $storage = $authAdapter->getResultRowObject();
    $storage->ip = $_SERVER['REMOTE_ADDR'];
    $storage->user_agent = $_SERVER['HTTP_USER_AGENT'];

    $storage = $auth->getStorage()->write($storage);
    return TRUE;
}
else
{
    return FALSE;
}

This will put your user object in your Zend Auth Storage along side the IP and User Agent.

Conclusion

Hope you liked it. I am very interested in hearing your thoughts about this system so you’re invited to leave your comments below or on my blog at http://phpdev.ro and I will respond as quick as possible.

See you!

10 Responses to “How to avoid Identity Theft in Zend Framework with Zend Auth”

  1. doomhz Says:

    I prefer to keep server data encrypted in md5 in the session and compare only the digests, not the actual user IP or browser type:

    $storage->ip = md5($_SERVER['REMOTE_ADDR']);
    $storage->user_agent = md5($_SERVER['HTTP_USER_AGENT']);

    and then

    if (self::$_secure_level == 3)
    {
    return $storage->ip == md5($_SERVER['REMOTE_ADDR'])
    && $storage->user_agent == md5($_SERVER['HTTP_USER_AGENT']);
    }
    elseif (self::$_secure_level == 2)
    {
    return $storage->user_agent == md5($_SERVER['HTTP_USER_AGENT']);
    }
    elseif (self::$_secure_level == 1)
    {
    return $storage->ip == md5($_SERVER['REMOTE_ADDR']);
    }
    else
    {
    return FALSE;
    }

    Also it would be great if you could get server data from the request plugin method:

    $this->_request->getServer(‘REMOTE_ADDR’, ”);
    and
    $this->_request->getServer(‘HTTP_USER_AGENT’, ”);

    Great script! ;)

  2. doomhz Says:

    I prefer to keep server data encrypted in md5 in the session and compare only the digests, not the actual user IP or browser type:

    $storage->ip = md5($_SERVER['REMOTE_ADDR']);
    $storage->user_agent = md5($_SERVER['HTTP_USER_AGENT']);

    and then

    if (self::$_secure_level == 3)
    {
    return $storage->ip == md5($_SERVER['REMOTE_ADDR'])
    && $storage->user_agent == md5($_SERVER['HTTP_USER_AGENT']);
    }
    elseif (self::$_secure_level == 2)
    {
    return $storage->user_agent == md5($_SERVER['HTTP_USER_AGENT']);
    }
    elseif (self::$_secure_level == 1)
    {
    return $storage->ip == md5($_SERVER['REMOTE_ADDR']);
    }
    else
    {
    return FALSE;
    }

    Also it would be great if you could get server data from the request plugin method:

    $this->_request->getServer(‘REMOTE_ADDR’, ”);
    and
    $this->_request->getServer(‘HTTP_USER_AGENT’, ”);

    Great script! ;)

  3. avaranger Says:

    1) The change should be NOT adding a simple if or sth. like that, but using the appropriate tool – Front Controller plugin with initSecurity implemented in dispatchLoopStartup() method ;)
    2) Yes, I’ve noticed that and no problem for me. I just humbly noted, that it doesn’t follow >Zend Framework< coding standards.

    Thanks for sharing your code ;)

  4. blooddevil2603 Says:

    1) I understand now what you mean and yes it is correct. But I guess all the code would know that the init will be called. In any case, it can be easily changed to NOT reinitialize anything if its the same request (I will actually make this change)

    2) Ah yes, but it is a matter of personal taste I guess. Personally, I have strict coding conventions where camel cased words are for methods and functions not for var. names. Thus the code :)

    3) I see what you mean now, and I 100% agree. Either composition will fix this OR having the identity stored in your class (thus no more Zend_Auth directly). Will change this also.

    Thanks again for the feedback! :) Cheers

  5. avaranger Says:

    First: The init method is called upon instantiation of the controller. Therefore there could be a situation (like _forward() to another controller) when it will be called twice in one request. I’m not saying that it will break anything using this code, … but i guess that regenerated id twice per request would be unexpected….
    Two: I’d expect the variables to be cammelCased (not like $_session_exp_time)
    Three: When you have My_Auth extends Zend_Auth and you call My_Auth::getInstance() you’d get Zend_Auth instance and not My_Auth instance. This is unexpected behaviour for me – i heard it got fixed in PHP5.3 by late static binding, but havent tested yet. I’d rather see it not extending anything and storing the Zend_Auth instance in member variable. But it you programmer decision. It might as well be an issue of personal taste :P

  6. blooddevil2603 Says:

    @seldaek

    I agree 100% on your POV. If you are developing a website that you want to be accessible via mobile browsers (and includes the auth.) then don’t use this class (since it is risky). But as you pointed out very well, this is a class that can mainly be used for strict access websites OR for where you can neglect the fact that some ISPs may change your ip. I for one don’t know any major ISP in Europe that does that (AOL OK for US). Cheers!

    @avaranger
    First – initSession is executed here in the base controller, which means it is ran every time the app. is executed. A controller extends Base Controller which extends Zend Controller Action.
    I’ve used forward a numerous times so far and it hasn’t done anything unexpected. Can you explain what you meant by unexpected? I cannot find anything wrong in this approach.
    Second – I am curios of why you say this? The funny thing is, i actually copy/pasted the Zend Auth class and modified it (using the same standards as in their class – you can check it out in the library)
    Third – My Class does NOT have the identity. Zend Auth has it all the time, my class never *has* it. It only checks for some more info’s inside of it. If you look in the Zend Auth file, you will see that there is nothing that could create unexpected behavior by adding 2 custom fields in the identity from Zend_Auth (That is why they even added the Zend Auth Storage where you can put whatever you want :) ). If you could explain more what you meant, I will gladly consider it

    P.S. Thanks for the feedback!

  7. avaranger Says:

    Thank you for this tutorial, but it has some problems.
    *First* – initSession method should be implemented in Controller_Plugin – in this way the code will be executed with every controller instatiation – which can lead to unexpected behaviour when dispatching more than one action (for example using _forward())
    *Second* – you code doesn’t follow ZF coding standards.
    *Third* – extending Zend_Auth is a risky business – it can have unexpected behaviour like that it changes default Zend_Auth. Meaning when you class has identity, Zend_Auth has it too… I’d prefer composition over extending.

  8. seldaek Says:

    AOL isn’t such a problem in europe for example, but however you have to take care of mobile devices that are used more and more, and that can change IPs randomly. This is a growing problem and imo if you don’t want to piss off your users you should never do IP checks. Obviously for some restricted admin panel it sounds acceptable, as long as you understand what you’re doing.

  9. blooddevil2603 Says:

    I agree and as I said, it might not be usable at all under certain circumnstances. But you might have a very strict admin panel where you really want to take care of things like this, where this could come in handy.

  10. ml Says:

    Relying on IP address and user agent to remain unchanged during a session of the same user to detect session stealing is not very feasible.

    The problem is that some ISP (notably AOL) use rotating proxies to access the Web. So your site will see different IP addresses for the same user during the same session.

    Also user agents may change. For instance, browsers that support external download managers may make the site see different user agents when a page or file is downloaded from the site.