As a follow-up to my previous article regarding User sessions, what data
should be stored where?, I wanted to discuss how to store the session, and
how to generate cookies that are tamper proof.
What are we trying to accomplish?
Ultimately we want to be able to have X amount pieces of data that are tied to
a particular user. Unfortunately due the fact that HTTP is a stateless protocol
we have to use cookies. Cookies are small little pieces of data that are
transmitted from the server to the client (generally done once), and then upon
the user coming back to the website they are transmitted from the client to the
server. This allows us to uniquely track a single user across connections to
our website.
If the website allows a user to authenticate and the fact that they are
authenticated is stored in the session, we also want to make sure that we can
aggressively expire a session, if this is possible depends on our session
storage.
Session storage
There are a multitude of ways to store the session data, but it ultimately
boils down to server-side or client-side. Server-side can be done in Cassandra,
Memcache, Redis or even in a SQL database.
Server-side storage
The main one that has
been used for years is to use server side storage. Storing a small file on the
servers hard drive that contains the data, and the client is sent a cookie that
contains a unique identifier that is linked to the on-disk storage.
For example:
1 => /tmp/session_1
2 => /tmp/session_2
...
N => /tmp/session_N
Easily expire sessions
The upside to server-side storage is that it is possible for us to very easily
expire a session, simply remove the associated file/data that is stored and the
users session has now become invalid.
Client-side storage
The other method that has recently started being used more to make it easier to
scale the server side is to store session data encoded in base64 in the cookie
itself. In this case there is no unique session ID, and no data is stored
server side.
Expiration is more difficult
The downside to using client-side storage is that there is no way, short of the
expiration on the cookie itself for the website to expire a session. There are
work-arounds, but they all require storing state server-side. A hybrid approach
for example is possible, store a unique ID along with the session data, and
store that unique ID server side, but none of the extra data. Remove the unique
ID server side and if we receive a session that contains a unique ID we don't
recognise, we simply clear the session.
Expiration, why do I care?
Being able to easily expire a users sessions allows for extra security
measures. For example in Google Mail it is possible to sign out all other
locations, this forces those other locations to re-authenticate before gaining
access to your account.
This is a good security measure to have, so that if a users cookie is stolen,
or their credentials are compromised upon changing their password all their
sessions are invalidated and an attacker using an old cookie/session ID can't
continue to wreak havoc on the users account.
Cookie format
If we are just storing a session ID, or the full session the cookie should be
hardened so that it can not be tampered with by a client. Even if you are
protecting the cookie using SSL, we still don't want to allow a malicious user
to modify the cookie to change the session ID or the session itself.
Signing your cookie
The single best way to make sure your cookie has not been tampered with is to
cryptographically sign your cookie, and upon receiving the cookie from the
client verifying that the signature matches what you are expecting. This is
especially important if you are using client-side storage, because you don't
want someone to be able to change the user ID from 950 to 1 and suddenly
impersonate a different user.
Use an HMAC
HMAC (Hash-based message authentication code) is an cryptographic construct
that uses a hashing algorithm (SHA-1, SHA-256, SHA-3) to create a MAC (message
authentication code) with a secret key. It is very easy given the secret key
and the original data to create the MAC, but it is very difficult if not
impossible to take the original data, and MAC and get the secret key.
This allows us to do the following:
data = "Hello World"
mac = HMAC(data, sha256, "SEEKRIT")
Our mac
would now be equal to:
e655f98cb9b3c02f45576f7906d64b0b7f8731f25a5319c42ca666917aca45a4
If we now create our cookie as follows:
cookie = mac + " " + data
It would look as follows:
cookie = e655f98cb9b3c02f45576f7906d64b0b7f8731f25a5319c42ca666917aca45a4 Hello World
We can then send that to the client that requested the page. Once the client
visits the next page, their browser will send that same cookie back to use. If
we split the mac from the data, we can then do the following operation:
cookie = e655f98cb9b3c02f45576f7906d64b0b7f8731f25a5319c42ca666917aca45a4 Hello World
data = "Hello World"
mac = e655f98cb9b3c02f45576f7906d64b0b7f8731f25a5319c42ca666917aca45a4
mac_verify = HMAC(data, sha256, "SEEKRIT")
mac_verify == mac
If and only if mac_verify
and mac
are the same can we be sure that the
cookie has not been tampered with.
This requires that the client is NEVER aware of what we are using as our secret
key. In the above exmaples that is "SEEKRIT". In your web application you will
be required to make this a configuration variable, and you will have to take
care not to commit that configuration variable to a git repository and upload
it to github (for example).
Do not use a bare hash algorithm
Using a bare hash algorithm allows for length extension attacks if used
incorrectly, this would allow an attacker to concatenate extra data to the end
of our existing data, modify the "MAC" and the server would accept it.
This construct is thus very dangerous:
data = "Hello World"
key = "SEEKRIT"
mac = SHA1(key + data)
The following construct is still not recommended, but is not nearly as
dangerous:
Due to the key being last, this is not vulnerable to a length extension attack,
however please don't do this, instead stick to using an HMAC instead.
Encrypting session data
When using client-side storage, it may be beneficial to encrypt the data to add
an extra layer of security. Even if encrypting the data you need to continue
using a MAC.
Using just encryption will not protect you against decrypting bad data because
an attacker decided to provide invalid data. Signing the cookie data with a MAC
makes sure that the attacker is not able to mess with the ciphertext.
What are web frameworks/languages doing by default?
I am most familiar with the Pylons Project's Pyramid Web Framework, the
default session implementation that is provided by the project is named
SignedCookieSessionFactory
, as the name implies this uses a client-side
cookie to store the session data, which is signed using a secret key that is
provided upon instantiation of the factory.
Flask sessions also uses a signed cookie for client-side session storage.
Ruby on Rails uses a signed/encrypted cookie for client-side session
storage by default.
PHP does not by default sign the session cookie, it does however use
server-side storage for session data by default. However extra security can be
added by installing PHP SuHoSin which adds session cookie
encryption/signing.