Security implications with CWebUser

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.

Hi donFuego, do not worry much about the -1 as I received aroun 15 down votes due to spammers in the past. There is nothing to do with the people here… I think there is a post where moderators explained that

I solved this problem by bypassing certain functions of CWebUser. I use the login functions as recommended but with my own password encryption (sha256 with salt and user email in it), I added a function for temporary passwords on password reminder, block the user, if he tries to access his account with a wrong password more than 5 times for 5 minutes*tries and and i love to work with flashes for error messages. Another reason is, that I want to enable login if cookies are disabled. I rewrite all urls with my own function and add the session id to the url if necessary (or strip and add it again). For security reasons I bind the session id to the ip and the user agent and clean all external urls from the referrer.

But I don’t use the auto login feature by default and save all states only in the session. I use the authmanager only for admin features, because I don’t want the user to show a non helping forbidden error. I use simple if constructions in controller functions and save the error message to flash and redirect the user from the forbidden page to the member index. In my opinion, this is much more userfriendly as I can describe the error reason better. And for me, chance is higher to forget the denys in the authmanager than in the controller functions.

I think, this is the idea of a framework. It should support the programmer and should not tell him, what to do. Yii makes a really good job with security problems, but I’m to paranoid to use just the basic model. And in germany we have some minimum goverment recomendation, e. g. not mailing the password, that I want to follow.

++ that!

There is a good community here, a few elitists if you like but mostly everyone is helpful. FWIW I think the +1 -1 ranking thing should be scrapped it just causes hard feelings. But if it is retained the identity of the person giving thumbs up or thumbs down should be public.

@donfuego don’t give up on us yet!

doodle

elitism? like how?

I find these forums are extremely friendly, and the dev team extremely responsive when problems and concerns are brought to their attention.

I wonder who gave you a -1, and for what - I poked through your posts and definitely didn’t find anything ignorant or offensive… You use a serious tone and make reasonable valid arguments… Please don’t ditch Yii (and us) because of one bad experience on the forums - you’ve been here for less than a month and made only a few posts. Whatever this is, I’m sure it’s a simple misunderstanding.

And you’ll be missing out on the best framework and one of the best forums there is for PHP frameworks :wink: