Security implications with CWebUser

I just realized that anything you put in the identity states of CWebUser persists as clear text in a cookie, if you use the "keep me logged in" option.

I was not aware of this security implication.

During run-time, identity state is kept on the server-side, in session variables.

Persisting it on the client-side is a very bad idea. For one, this data can too easily get stale - suppose a subscription expires between logins? Or any other kind of account status change, for that matter.

For another, any data you keep there, is going to be exposed as plain text on the client machine.

I don’t know why it was implemented this way - it seems like a really bad idea. In my own login systems in the past, all I’ve ever kept in a cookie is a hashed user-id and a temporary key of some sort.

Under not circumstances would I ever consider persisting e-mail addresses, physical addresses, or other sensitive information.

Is this documented? The documentation should include a warning in giant red letters and <blink>.

It is documented, see the green box on this page: http://www.yiiframework.com/doc/guide/topics.auth

But i agree to you that i don’t fully understand why every state is saved in cookie, just to allow cookie based authentication. This is still one of the more obscure parts of the framework to me ;)

Another problem is, even if you enable HMAC cookie-protection, you can replace the current cookie with an old cookie. Means you are able to get assigned a state you had in the past (eg username, user level etc).

Please see my ticket regarding this issue. I think the best solution is to work with a server-side token. Means only save the token on client-side and read the actual data from a database.

Another tricky part was when I had to implement impersonation for a client - the ability for an admin user to temporarily act as another user.

It was doable, but rather tricky - there is no direct way to simply make a user active, without "faking" the login process, by overriding certain methods. It seems like setting a user "active" should be a simple one-step operation provided by the framework, not something you have to hop through hoops to achieve.

Had there been a direct way to log in a user, without authentication, the "keep me logged in" function could have been implemented using simply a random key and a hashed user id, avoiding the persitence issue - that code would have looked a lot cleaner.

Thought comes to mind: perhaps the design of the user/authentication architecture attempts to mimic too closely the actual end-user’s process of logging into a website.

Login, user and authentication components should probably be implemented more in terms of “units of work”, more from the application’s perspective, e.g. “make a user active”, “generate an auto-login key”, etc.

Just a thought…

What you think about adding CWebUser::setLoginState()? States set with this method will be stored in the auto-login cookie (and session of course). User id & name will continue to get always stored in the cookie since they are always the same for most applications (unless an admin changes the username manually).

For non-login states, one may use CWebUser::afterLogin() to populate the session (eg read some permissions from the database and then call CWebUser::setState()).

With this solution, there’s also no need to encrypt the cookie data because there’s most likely no usage case were you want to store sensitive data as a login state?

Login-states I can think of:

  • user settings like timezone (these settings will most-likely not change until the user does it manually)

  • country-code (evaluated by the server and saved in cookie in order to save CPU time on further requests)

I ran into this problem when Yii tried storing so much info in a cookie that it caused browser problems.

My workaround: extend CWebUser as follows:


	/**

	 * Populates the current user object with the information obtained from cookie.

	 * This method is used when automatic login ({@link allowAutoLogin}) is enabled.

	 * The user identity information is recovered from cookie.

	 * Sufficient security measures are used to prevent cookie data from being tampered.

	 * @see saveToCookie

	 */

	protected function restoreFromCookie()

	{

		$app=Yii::app();

		$cookie=$app->getRequest()->getCookies()->itemAt($this->getStateKeyPrefix());

		if($cookie && !empty($cookie->value) && ($data=$app->getSecurityManager()->validateData($cookie->value))!==false)

		{

			$data=unserialize($data);

			if(isset($data[0],$data[1])) {

				list($id,$password)=$data;

				$identity = new UserIdentity;

				$identity->id = $id;

				$identity->password = $password;

				$identity->authenticate();

				

				if ($identity->errorCode == UserIdentity::ERROR_NONE)

					$this->changeIdentity($identity->getId(),$identity->getName(),$identity->getPersistentStates());

				else

					throw new CHttpException(500,'Bad cookie information.');

			}

		}

	}


	/**

	 * Saves necessary user data into a cookie.

	 * This method is used when automatic login ({@link allowAutoLogin}) is enabled.

	 * This method saves user ID and hashed password

	 * These information are used to do authentication next time when user visits the application.

	 * @param integer number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser.

	 * @see restoreFromCookie

	 */

	protected function saveToCookie($duration)

	{

		$app=Yii::app();

		$cookie=$this->createIdentityCookie($this->getStateKeyPrefix());

		$cookie->expire=time()+$duration;

		$data=array(

			$this->data->id,

			$this->data->password,

		);

		$cookie->value=$app->getSecurityManager()->hashData(serialize($data));

		$app->getRequest()->getCookies()->add($cookie->name,$cookie);

	}

Basically it only saves the user id and password to the cookie. Then on restore from cookie, it verifies the password and then calls changeIdentity

Isn’t this most dangerous solution to store password in the cookie?

If the cookie will be stolen (physically or by some sniffer) then account will be compromised and hacker will have both user name and password.

Also this approach does not realy solves the problem (see below).

I thought the problem with cookie can be solved this way:

  • all user states are saved to the session

  • cookie contains only PHPSESSID

  • when we need autologin the user we open session and check if user ID is present

  • if we found user ID in the session then we automatically log user id

But I just reread some articles and manuals about PHP sessions and the problem with this approach is that session file can be destroyed before cookie is expired.

In this case we will get PHPSESSID from the cookie, but will not be able to get user state from the session.

Also this will break solution suggested by jonah - user name / password will be in the cookie, but no other saved user state will be available since session file is deleted.

PHP has two important session settings to control session lifetime:

  • session.gc_maxlifetime - how long session file will exist (default 1440 seconds)

  • session.save_path (by default some temp folder used by all applications)

The hidden problem here is that if we set session.gc_maxlifetime to some value, but do not change session.save_path then real session lifetime can be overriden by other application.

See details here and here.

So to keep session files as long as cookies we need to:

  • set session.gc_maxlifetime to value appropriate to cookie expiration

  • set session.save_path to specific folder for application

I think with such settings it should be possible to implement saving all states to the session and expose only PHPSESSID to the cookie.

But this leads to other problem: session files will exist on the server for long period of time and if we have many users then session files can take much disk space.

This way current yii’s implementation is good compromise:

  • if no cookie-based login is required then user data is stored in the session. This way you can notice sometimes that even without closing browser you can become logged off (because php deleted session file).

  • if we enable cookie-based login then all state is saved in the cookie so application is not depends any more from session lifetime. If session is deleted then new will be created and populated from the cookie.

And regarding current solution suggested by qiang (see here and here).

Maybe this solution can be implemented as part of yii core?

What I usually do is, at login, I generate a random key and write it to the User record.

I give that random key to the client in the form of a cookie.

To resume a session, the current user key has to match that of the cookie.

This has the added side-effect of being able to save your login on only one machine - so that, if you left some other machine logged in, by the time you log in from your own machine, the other machine can no longer resume that session.

That may not be desirable for everyone, but that’s how most of our clients prefer it.

I want to come back to this initial topic. From my observation this is not true. Only those states set with CUserIdentity::setState() will get saved to the identity cookie! If you save a user state with Yii::app()->user->setState() it will not be part of the cookie.

It’s not so easy to understand the code in CWebUser that’s responsible for this. But let’s try:




// in login():

$states=$identity->getPersistentStates(); // only contains states set in the identity class

...

$this->changeIdentity($id,$identity->getName(),$states);


// leads to changeIdentity():

$this->loadIdentityStates($states);


// leads to  loadIdentityStates():

$this->setState(self::STATES_VAR,$names);  

// so here $names will contain the list of names of persistent UserIdentity states from above




// now continue in login():

 if($this->allowAutoLogin)

    $this->saveToCookie($duration);


// leads to saveToCookie(), where cookie data is composed in $data, and only contains:

$this->saveIdentityStates(),


// leads to saveIdentityStates():

 foreach($this->getState(self::STATES_VAR,array()) as $name=>$dummy)

            $states[$name]=$this->getState($name);

// so here we read back the states saved above in loadIdentityStates() with key self::STATES_VAR

// which are the states set in identity class



Maybe someone can confirm this. I’ve also asked this in a ticket but couldn’t get a really definitive answer to this yet.

Yes, It seems like user states are saved to the cookie during login only.

But we also have a possibility to set some states before login or during login (for example, if we override beforeLogin() method). So states set with CUserIdentity::setState() and states set with Yii::app()->user->setState() before login will be saved to the cookie.

Also this may lead to some state inconsistence - is php session is expired then only part of state will be restored from the cookie.

Update:

I was wrong.




foreach($this->getState(self::STATES_VAR,array()) as $name=>$dummy)

            $states[$name]=$this->getState($name);



Here loaded only those states which previously taken from CUserIdentity and saved to separate array in the session.

So even if we use Yii::app()->user->setState() before login then such state does not saved to the cookie.

The whole thing is much harder to understand that such a thing ought to be. I’m afraid it’s a case of over-architecting to some degree… Personally, I’d prefer something simpler and more transparent.

I think the basic idea for the design was to keep it open to all kinds of authentication mechanisms. Jeff enhanced the guide on this topic (which will be online with 1.1.5 or can be found here) to make it more clear, how the componentes interact. The basic design is not that bad. But some parts - like the mentioned state stuff - still confuses me.

As i see it, we now have 3 different ways to store persistent information with a user (maybe a 4th, if you add direct access to $_SESSION):

[b]Yii::app()->session /b

The wrapper for easier access to $_SESSION

Yii::app()->user->set/getState() (CWebUser):

Another interface to the user session

CUserIdentity::get/setState():

A third interface to the session with the additional feature, that data saved here will also go to the cookie if cookie based authentication is enabled.

All that’s missing here are some real life examples of when to use what. There must have been some rationale behind this state stuff.

I find this post quite interesting, please allow me to input:

Wouldn’t be possible to encrypt data stored in the cookies before it is saved with a Key and Salt values configured in the main.php file?

Then, we could encrypt data (hashed-whatever), cookie is saved on the client having the encrypted data that afterwards is matched against a DB table that yii creates if necessary named (for example) yiistates. This table holds the ID of the user and the value of the encrypted data (plus other extra info). If there is a match… well you know the rest.

Just dropping ideas (sorry for my english)

You can already do so:

http://www.yiiframew…tack-prevention

Also see CSecurityManager where you can configure your own keys (through core app component ‘securityManager’).

EDIT:

This might not encrypt the data, though. Only prefix with a hash.

"Yii implements a cookie validation scheme that prevents cookies from being modified." but not from being sniffed… am I right?

The data already gets hashed - so you can’t tamper with it.

But it gets stored in a legible format, so you can only store data that isn’t sensitive in the first place - there isn’t typically a lot of data that falls right in the middle: safe to view and store, but not safe to manipulate. Typically only “insignificant” data can be stored this way, like your user ID, or your current geographical location…

looks like i have to solve something similar right now.

I’m using ‘external’ server to authenticate users (implemented SSO conception, because i need SSO. For example, api - differ subdomain, ‘trusted site’ - differ domain, so we have differ sessions, that’s why i had to implement SSO to have one session on server side). User authenticated on one site will be also authenticated on other site.

Everthing is fine with ‘login’, but now i puzzled with ‘logout’. When i logged out at one site, i’m not automatically loggedout on all sites (if they uses Yii), cause ‘restoreFromCookie’ is never happen due to fact that getIsGuest in init of CWebUser is true (because it checks session). But i need check ‘auth server session’, not session of Yii application. So i stucked with situation when:

a) i logged out at auth server, but

B) i still logged in in yii application

and if i have 5 sites with yii, but all of them uses same auth server, i will be easily with situation: logged out at auth server, logged in at 3 sites and logged out at rest 2.

Right now i have to rework ‘init’ of CWebUser to call ‘getIsGuest’ AFTER checking server.

Hi! I’m Picking up the thread again…

There seem to exist some ambiguity in the opinion whether session-data gets stored in (clear text) cookies under certain circumstances or not among the "senior" developers.

I’m a newbie looking for, what seems to be, a hard-to-find answer on a very simple question:

[b]

  • How to (securely) store server-side session variables in Yii?[/b]

…and how to best manage that ‘session’.

Yeah, thanks for the -1. I will scrap Yii after finishing this one. There’s way too much elitism going on here. It’s a shame on a nice framework.