![]()
Our session system is due for an upgrade. Currently all PHP sessions are stored in the database, and some things are getting a bit slow. There have been a couple of approaches I've been considering, one of which is simply storing all the information in a browser cookie.
First I want to make clear I don't necessarily condone this. The reason I'm writing this post, is because I'm hoping for some more community feedback. Is this a really bad idea? I would love to know.
The benefits
If all the session data is stored in the browser, it means that I don't need to store it on the server. I actually don't care all that much for having the data on the server (unless it's the only secure way), it's mostly a gigantic map with session tokens and user id's (along with some other info).
I also feel it's more natural for HTTP, as it makes it a bit more stateless.
Sample code
- <?php
-
- class BrowserSession {
-
- public $secret = 'this will need to be a cryptographic random number';
- public $currentUser = null;
-
- // Sessions time out after 10 minutes
- public $timeout = 600;
-
- function init() {
-
- if (!isset($_COOKIE['MYSESSION'])) {
- echo "No session cookie found\n";
- return;
- }
-
- list($userId, $time, $signature) = explode(':',$_COOKIE['MYSESSION']);
-
- // The cookie is old
- if ($time> time() + $this->timeout) {
- echo "The session cookie timed out\n";
- }
-
- if ($signature !== $this->generateSignature($userId,$time)) {
- echo "The secret was incorrect\n";
- }
-
- $this->currentUser = $userId;
-
- echo "Logged in as user: $userId\n";
-
- }
-
- function login($userId) {
-
- $this->userId = $userId;
-
- $time = time();
-
- $cookie = $this->userId . ':' . time() . ':' . $this->generateSignature($userId,$time);
-
- setcookie('MYSESSION',$cookie,$time+$this->timeout,null,null,null,true);
-
- echo "Set cookie: $cookie\n";
-
- }
-
- function generateSignature($userId,$time) {
-
- $stringToSign =
- $userId . "\n" .
- $time . "\n" .
- $_SERVER['HTTP_USER_AGENT'] . "\n" .
- $_SERVER['REMOTE_ADDR'];
-
- return hash_hmac('SHA1',$stringToSign,$this->secret);
-
- }
-
- }
-
- ob_start();
- $session = new BrowserSession();
- $session->init();
-
- if (isset($_GET['login'])) $session->login($_GET['login']);
- else {
-
- echo '<br /><a href="?login=1234">Log in as user 1234</a>';
-
- }
- ?>
A few notes:
- The preceeding code was just intended as a proof of concept, it's missing some validation.
- Currently the secret would be the same for every user. I was thinking of appending some per-user information to the secret. If somebody does guess or bruteforce the secret, they would only have access to a single users' information.
- If a user changes their password, existing sessions should expire. To do this the signature should also include a sequence number that changes when the password changes.
- Currently this only stores a user id. It could be extended to contain more data, but this is all I need.
So, is there anything fundamentally wrong with this approach? In general the client should never be trusted, but for setups where the security requirements aren't as high (highly subjective, I know) I feel this might be strong enough. OAuth, OpenID and Amazon AWS all seem to trust HMAC+SHA1, but those applications do work differently.
Credit where it's due
I first asked this question on stack overflow. The users there already gave some great suggestions and pointed out some of the flaws. Thank you!

There are limits on how much data can be in a cookie and how many cookies can be used. (IIRC the netscape cookie std limited it to 20kb and 20 cookies per domain, something like that).
Other than that there's nothing terribly wrong with it as long as private data is encrypted as do are already doing. It might open your site to a new class of attacks, but the likelihood of them ever working is improbable.
I just read the stack overflow replies. It's important to know that this (or any session scheme) won't protect you from other people fiddling with your users' data. HTTPS is the solution for that. It will protect you against people fiddling with their own data.
"it's mostly a gigantic map with session tokens and user id's (along with some other info).?"
Did you consider the traffic increase? Additionally EVERY get request to the site will send much more data, taking more time of the user-experience. This becomes an issue also if images, stylesheets and such are on the same cookie domain.
The only thing I would be concerned with, is the ease in which a user can adjust their cookies. So if in your cookie you put credits=3.. then the user could adjust that to 20 and have 20 credits to use.
I'm not sure if that means anything to your site in particular, but it'd be the only real concern of mine.
Also the whole "never trust user submitted data" would need to be addressed on every page load because what if you had name=CW and then all of a sudden I change my name to Tonya and you're just using the name variable to display or write to the db.. All of a sudden you're getting Tonya down as my name on order forms, forums, etc, even though it's CW.
I generally consider a bad idea storing data in the client cookies, because it increase the HTTP overhead on each request to the web server. For instance if you store 512bytes of data, a page with 50 objects (css, js, images) would send and receive 50*512=25Kb more for every request, something you can "feel" on a 3G connection.
Then you might consider that even if you encrypt the data, a stolen computer (or just an internet cafe' pc) can be victim of a brute force attack on stored cookies to extract any useful information, so you should not store any important information on the client. Consider also traffic sniffers, you probably don't want to send your users information over the network, possibly through proxies or unsafe wifi networks.
I'm sure you have good reasons to consider this approach, however you are supposing that your visitors have a fixed IP, the truth is that the IP can change in the middle of a session and the user should not be penalizes with a logout. At the same time you are logging out all the people who upgrade their browser, e.g. from Firefox 3.6 to 3.6.1, because the useragent changes and the "signature" is no more valid.
It's not a good idea to trust the cookies to store the user id! ;-)
@Alan Trick/Beberlei:
Bandwidth is a good point. In my particular case no really a concern. The sample's code produces cookie sizes of about 60 bytes, which is not bad.
@CW:
If a user adjusts their own cookies, the signature will be incorrect and the request would be rejected. There's no way to change the data without also being required to calculate the signature, which is impossible (or very hard at least).
@Davis:
The example actually expires cookies after 10 minutes. It wouldn't be a problem to make the sessions expire when the browser closes either, which brings it on-par with every other session system out there.
I added the User agent as an additional precaution. You could totally leave it out of the signature, but users getting logged out because they upgrade their browser is also not a major concern for me.
Your points about traffic sniffers (man in the middle attack) always applies, even for 'regular php sessions'. HTTPS solves that, cookies won't.
Any reason you cannot put the session data in memcached? You maintain the network access and scalability while using a program tested to work under huge loads.
@Martin: Memcache could work too, don't really have anything against it other than it makes inter-datacenter database replication harder (which is not a major concern, but is something we're dealing with).
Something NoSQL related would be cool too, I'd love to get more experience in that area anyway =)
I've used MongoDB on some rather large sites for session handling in the past and it works wonders. I would suggest looking into that. NoSQL is rather easy to conquer and very powerful as well as scalable. In one site that gets about 1/4 million PV per day, I use Mongo to handle sessions and track visitors. Makes for a perfect combination.
I can't remember if it was Sitepoint or not, but someone posted an article on database session performance. Essentially, they were able to get a huge boost by simply changing the engine type for the session table to MEMORY. I know you're wanting to keep session data off the server, but with the average session taking up 60bytes, I don't see why it would be a major concern. You'd have to serve millions of sessions before having issues with session storage.
I think Flickr is using this approach and was praising it on various occasions, RoR has it also and it was introduced as a big feature IIRC. Would be good if someone in the PHP world would do a decent lib for that with an ability to make an easy switch between storing some data in encrypted cookie and other (more sensitive) on the server side.
1. Im confused. So does it mean that i cant click around for longer than 10 minutes? Seems like you add time() to cookie when login happens. Then never updating it means it will be invalid after 10mins even if you click around.
2. I would put it all through AES so cookie has base 64 or gibberish with no readable data. Leaving userid/time open-text is minor but unnecessary exposing of information.
3. I would use memcached or mysql with indexes and key distribution over several servers if its such a big table.
Memcached has a drawbacks: its not persistent and it evicts entries on memory shortage.
Mysql - it is really just an index lookup so queries will run at light speed. Add tons of ram to mysql and sessions will fly too!!! writes are worse but you can limitate writes with custom session handler and update DB only when state changes not on each call. If you really dont have to store a lot of data make session table fixed length to improve speed. Experiment with MyIsqm/InnoDB for best read/write ratios.
4. Cookie session will be too small and will have no way to store any application state, form values, preferences or whatever. This way you will increase load on the other DB sections to store all these pieces.
Still, a good thing about cookie sessions approach is that you dont have to call db/memcache at all : -)
Art
Arthur:
1. The timeout is currently 10 minutes. You are correct about that the cookie would just go away in this case though. It should actually send a new cookie every time, so the window increases.
2. I went for this approach instead of something like AES really because OAuth and Amazon use it. I don't know enough about it to make this decision on my own, so I just went for it. None of this information is confidential though, I just don't want anyone tampering.
3. I'm exploring multiple options. So far this is my favorite because it doesn't require additional server-side infrastructure.
4. I'm also not too concerned about storing application state in a session. In my case my needs are very specific (just need the user id).
Thanks!
...et Project 2010 (WIP2010) Armazenamento de Informações de Sessões Cifradas Aqui fica uma boa dica sobre guardar informação de sessões em PHP, de uma forma cifrada. Vale a pena uma olhadela! ...
There are several problems with your solution, but it really depends on your needs and your architecture, which I don't know. I'll just point out some problems and then you can judge them for yourself.
1- The secret must be big and good enough (mix of digits, lower/upper alpha, other ascii chars). If your secret is for example 8 or 9 digits, I can quickly bruteforce your key since I have the cookie value and I know how to create the signature (If I have the clear text and the hash, the secret is the only variable left in the equation). I would also use sha256 for the hmac.
2- This method is pretty static, i.e., you cannot have anything in the cookie/signature that might change during the user's session (e.g. name, etc).
3- Assuming that the 10 minutes is intended to be a timeout (and not actually the lifetime of the cookie), you will need to rewrite the cookie on every request.
4- If you need to ban a user immediately you can't, since the state is on the client side. If a user finds that his account was compromised and changes the password immediately hoping the bad guy will be kicked out, he'll be frustrated. Your idea of having a sequence number will force you to query the database every time you validate a user session (unless I missed something in your explanation)
5- As others point out, there's a traffic increase as well
But as I said in the beginning, all this might not be an issue, it really depends where you want to use this on.
Hi Nuno,
That's a very good summary of the issues. I'll respond quickly to the ones relevant to me:
1. sha256 is a great idea. I was definitely intending to use a very complex cryptographically secure key.
3. I do intend to recreate the cookie every time a user accesses the site. 10 minutes is a bit low, so I would probably increase that quite a bit.
4. One thing I mentioned is that I wanted to add a sequence number to the secret (per user). If the user changes their password (or I ban them) I can increase this number and they would be instantly locked out.
Evert,
3- On normal PHP sessions you usually have a timeout (not expiration time) and the cookie is a session cookie (will expire once you quit the browser)
4- I'm sorry, I'm still not sure if I got it right. That sequence number must be stored in the database right? If so, you need to query the database on every request (so you have the sequence number to validate the cookie). In that case, isn't it the same as storing the state on the server side (which is what you want to avoid)?
we've moved to using client-side cookies for a while now, mostly because shared hosting isn't all that secure, and using mysql sessions isn't always ideal.
in the cookie we store the user ID and maybe a few bits of status info, a CRC, and a timestamp.
The cookie is encrypted using mcrypt with rijndael 256.
Also, static content should be served off a different sub/domain, preferably off a different IP. Using this approach will ensure that the cookie is not passed to the static server.
I think the real deciding factor is how much data will be stored. If you're storing not-small arrays then server-side is the only way to go.
Cheers.
Nuno,
3 - The problem with session cookies is that they will always remain valid. The server can't expire sessions the usual way, so for security sake it's a good idea to expire.
4 - You are right, the sequence number is pulled from the database (and cached). Our issue isn't database reads, but storage and writes.
Gotcha.. makes sense.
I only post this because it wasn't covered in the stackoverflow discussion.
@Alan Trick: HTTP specification states that a HTTP header maximum size of 4KB. However, many browser implementations and firewalls (notably CheckPoint Firewall 1) limit it to 1KB.
Therefore, all your cookies, plus URL, plus User-Agent, etc must live within that 1KB if you want an easy life. It can be done if all you are looking to do is create some form of encrypted token that lets you know the username/userid/expiry/checksum without going near the database.
However, I don't see how you can implement token expiry / sequence numbers *without* hitting the database to check that... And since this is a database load reduction endeavor - if you're going to hit the database to check if the sequence number has changed, then you may as well pull the rest of the data in at that time also.
Finally, just a general comment. Rather than use a visible userid:time:signature approach, I would use true cryptographic method on the entire cookie. Since you are using PHP, you can create a hash with userid, time and other data, serialize() it, (optionally bzcompress because this compresses well), then encrypt using mcrypt() using e.g. 3DES, and finally convert to base64 and use that as your cookie value.
Reading the cookie is simply reversing the process and you have your variable back.
Good luck.