Using Encrypted JSON Data To Hide Cookie Implementation In ColdFusion
When it comes to authenticating users in our ColdFusion web applications, we are pretty much limited to the exchange of browser cookies. These are the bits of information that the browser submits to the server along with every request. We can encrypt these values to make them more secure; but, I wonder if the name-value nature of the cookies lends poorly to security? What if, rather than passing several encrypted cookies, we passed only a single encrypted cookie that represented all of the user data? Since ColdFusion 8, we can easily serialize and deserialize JSON data - perhaps this data format would be the perfect form of encapsulation; a way to hide the actual implementation of our secure cookies.
To experiment with this JSON-cookie approach, I have built a single ColdFusion page that implements a very dumbed-down user authentication life-cycle. If the user is not authenticated, we start off by building a simple ColdFusion struct. This struct contains the user's ID (assuming they just logged-in) and some additional information for security purposes:
- ID - The business-specific ID of the user.
- IP - The remote IP address of the user at the time of authentication.
- Date - A string representation of the login date.
- NumericDate - A numeric representation of the login date.
- Rand - A random string used to add variation to the data structure.
This struct is going to then be serialized as JSON data and stored as a user cookie. This cookie will then be passed back with each request where it can then be decrypted, deserialized, and examined for tampering. If any part of the resultant struct has changed, we can assume that the cookie has been manipulated by an external source and is therefore no longer valid.
<!---
Set up the user ID. For the purposes of this demo, we will
consider a user to be logged-in if they have a non-zero ID.
--->
<cfset request.userID = 0 />
<!---
Define the encryption key. This will be used to encrypt and
decrypt the data stored in the user's cookies.
--->
<cfset encryptionKey = "WhatchaGonnaDoWithAllThatJunk" />
<!---
Check to see if the user-data cookie exists. This is the cookie
that contains their login information as well as some location-
specific data for validation.
--->
<cfif !structKeyExists( cookie, "user" )>
<!---
The user does not have any cookie information so, for this
demo, will just create it assuming that the user is logging
in. Rather than just storing a simple value, we're actually
going to be storying and encrypted, serialized struct.
When we define this struct, it includes some additional
information to make sure that the encrypted data is not
altered manually.
NOTE: The "4" for userID is the assumption that the user has
logged in and has the Database ID of 4.
--->
<cfset userData = {
id = 4,
ip = cgi.remote_addr,
date = dateFormat( now(), "m/d/yyyy" ),
numericDate = fix( now() ),
rand = randRange( 111111111, 999999999 )
} />
<!--- Now, serialize the user data as JSON. --->
<cfset serializedUserData = serializeJSON( userData ) />
<!---
And, encrypt it. Because the entire set of JSON characters
work towards one object, it makes the ability to mess with
the encrypted data much more difficult.
--->
<cfset encryptedUserData = encrypt(
serializedUserData,
encryptionKey,
"cfmx_compat",
"hex"
) />
<!---
Store the encrypted user data cookie on the user's machine.
This will be passed back on every request, which can then
check against the request data for validation.
--->
<cfcookie
name="user"
value="#encryptedUserData#"
/>
</cfif>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
At this point, we are going to assume that the user has been
logged in and is now passing the "user" cookie data back with
each request. At this point, we can look at that cookie to make
sure that it is valid.
--->
<!--- Check to see if the user cookie exists. --->
<cfif structKeyExists( cookie, "user" )>
<!---
We are now going to try and decrypt and deserialize the data.
If any part of this goes wrong, we are going to assume that
the data has been messed altered by an external system and
is not longer valid.
--->
<cftry>
<!---
Decrypt the user data. This will result in our JSON
string representation.
--->
<cfset serializedUserData = decrypt(
cookie.user,
encryptionKey,
"cfmx_compat",
"hex"
) />
<!--- Deserialize the JSON data. --->
<cfset userData = deserializeJSON( serializedUserData ) />
<!---
Now that we have our deserialized data, we need to
compare some parts of it to make sure that it has not
been tampered with. If any of this check fails, the
entire cookie will be considered invalid.
--->
<cfif (
!isNumeric( userData.id ) ||
(userData.ip neq cgi.remote_addr) ||
!isNumericDate( userData.date ) ||
!isNumericDate( userData.numericDate ) ||
(fix( userData.date ) neq fix( userData.numericDate ))
)>
<!---
Either the IP address doesn't match, or the dates
don't match. In any case, something is not cool -
raise an exception.
--->
<cfthrow type="InvalidUserCookie" />
</cfif>
<!---
If we've gotten this far then the user cookie is valid.
We can now use any part of it that we need to initialize
the request.
--->
<cfset request.userID = userData.id />
<!---
Catch any errors thrown by the user-data-comparison
process. This might be unexpected or manually raised
exceptions.
--->
<cfcatch>
<!--- Delete the cookie from the user's machine. --->
<cfcookie
name="user"
value=""
expires="now"
/>
<!---
Reset the user ID to make sure they are no longer
considered to be logged-in.
--->
<cfset request.userID = 0 />
</cfcatch>
</cftry>
</cfif>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<cfoutput>
<!---
Check to see if the user ID indicates that the user is
logged-in (non-zero) or logged-out (zero).
--->
<cfif request.userID>
<!--- The user is logged-in. --->
You are logged-in: #request.userID#.
<cfelse>
<!--- The user is logged-out. --->
You must sign-in!
</cfif>
</cfoutput>
When we run this code for the first time and look at the cookie headers that come back, we'll see something that looks like this (I have broken it up on several lines for display purposes):
Set-Cookie
USER=605D72BB05DD52E10F1717B5BFC726FCD7FFA81363E12B1
DA78D00DF818FF53ED900EB1CFB100AEDD4FE2AC358B97EA12AD
10E99ADAFD3C1AAF1E0EAEAC81CE31EB9AC5298B4958DA326A37
BEF28F8FAEDFA2F070B4A;
path=/
As you can see, this encrypted cookie reveals nothing about the internal structure of our user data. Rather than passing each value back individually, where the names of the cookies can provide proprietary information, we are passing back only a single, encrypted value. Furthermore, since the encrypted value represented a structured piece of information, messing with any part of it will be much more likely to result in data that can no longer be deserialized or is no longer considered valid on the server.
For each subsequent request, this cookie is passed back to the ColdFusion server where it is decrypted, deserialized, and examined. If the IP address has changed or the two date formats no longer line up, the ColdFusion application considers the cookie to be corrupt and automatically logs the user out of the system (deleting the corrupt cookie and resetting the user's ID).
I like this approach because it really hides the way in which the cookies are being used. By passing back a single value, rather than multiple values, the name and number of persistent data-points can be changed within the application without providing any implementation details to the end-user. For this demo, I only showed a proof-of-concept; perhaps in the next demo, I'll work this into a true ColdFusion application setting.
Want to use code from this post? Check out the license.
Reader Comments
What's the point of embedding information like IP and login date in the data?
That seems like it would be better served just storing as session-ish data.
IMO, the safest thing is to store the bare minimum as a cookie (preferably only session id or a token identifier that prevents session stealing if URLs might contain session ids.) Then use the server to hold the rest of the values.
The bigger the cookie is, the more overhead you introduce in each page request. Obviously, the more data your encrypted, the slower the encrypt/decrypt process will be.
@Dan,
The thought was simply to make the alteration of the cookie more difficult. If the user started to mess with the encrypted value, I figured the chances were higher that the alteration would invalidate the cookie as a whole. Also, the internal structure might lend better to protect against decryption attempts?
Perhaps it's entirely overkill. It was simply an exploration.
As far as speed, I think the amount of data is insignificant enough to not be cause for concern.
Are we going to be affected by http://www.bbc.co.uk/news/technology-12668552
@Brian,
I didn't read the whole article, but I've heard bits of this being discussed on the radio. Honestly, I don't care for the huff people get into when talking about cookies and privacy. I think it's silly.
@Ben:
I'm not sure this really offers anything more than storing a really long secure key in the cookie. For example, you could hash some session information and basically get the protection you're looking for.
For example, if you stored 2 cookies:
cookie.SessionId = SessionId;
SecureKey = hash(
cookie.SessionId
& ":" &
session.loginDate
& ":" &
session.ipAddress
& ":" &
randRange( 111111111, 999999999 )
);
session.SecureKey = SecureKey;
cookie.SecureKey = SecureKey;
Even hashing that much data is probably a bit of overkill, but I think you'd gain the same level of tamper proofing you were looking for.
Now you just need to compare if the SecureKey has been unchanged for the session, if it's not valid then you throw an error.
If someone happened to successfully change a sessionId to something that happened to be valid, they'd also need to get the correct hash value. Of course, someone who's packet sniffing and the data is going over HTTP (not HTTPS) would be able to steal the session anyway.
Just having some kind of addition "SecureKey" cookie should be enough from someone randomly guess a valid Session ID.
If you're that worried about storing sensitive information, why not just enable server cookie management and turn off the client side management? Then the only thing the client can view in the cookie is an id which is handed to the server, so the server knows which cookie information to link to the client. All the actual cookie data is then stored on the server. This is how I do all my cookie and session management.
Neat idea, you just need to be careful. I believe that cookies have a 1K limit. Very neat idea though.
@Dan,
Fair enough - I'm sold. I like the hashing idea a lot. I had not considered that; though, I suppose that is what I keep having to do when I send things to various oAuth APIs (damn them and their complex hashing / signatures).
Thank you for the enlightening feedback as always!!
@Shelby,
As @Dan is saying, you still need *something* additional to make sure that people aren't just guessing session IDs.
One issue I see with this is that by linking the session to a specific IP address, you disable the ability to roam with a device. For example, if I log in from my 3G iPad on WiFi at home, then take it with me in the car and it switches over to the cellular network, my IP will change and suddenly my session will be invalid. It used to be true that some dialup providers would use proxy servers that could see a single user come from any one of several IP addresses within a single browsing session. In any case, it's certainly something to watch out for.
I do like the idea of hiding the values, though encryption of the full value may be overkill. I've done something similar in the past where I created a semicolon-delimited list of name-value pairs, appended a salted hash of the entire string for tampering comparison, then base-64 encoded it before sending it along. This was used to provide authentication on one website, and then to redirect the user to another application which would convert the string back to text and compare the hash to the rest of the string (we had a shared salt "key" to prevent tampering with the data). If it matched, they would set the values on their end and consider the values trusted. If the hash didn't match, they would redirect back to us to re-authenticate the user. (Part of the data was a timestamp in GMT which the receiving end could use to expire the token as well if needed.)
This is a pretty similar concept and I hadn't considered using it to store cookie data.
Also, the lowest common cookie value size is 4000 characters, so you'd have to be sure the encrypted/encoded value didn't exceed that length or you'd run into problems with managing sessions in some browsers.
As for why you'd store all this data in a cookie rather than server session/memory variable, this allows for simple load balancing between servers without having to be concerned about the servers sharing session data. You can easily cluster a bunch of "standard" edition servers together behind a load balancer without having to worry about sticky sessions or any of the clustering features of the application server.
@Justin,
I had never considered a mobile device that switched between cellular network and WiFi network; yeah, I suppose that would require a switch between IP address. Good thought, especially for the modern age of smart phones.
I don't really deal with load balancers, so again, thanks for some good insight. That does make sense with the cookie passing (as far as cookie vs. server).
That said, I think I like @Dan's ideas. They seem to hit the sweet spot of safety and overhead.
@Ben/Justin:
Justin brings up a good point, especially considering apps like Firefox Home for the iPhone--which allows you to sync sessions between your desktop and mobile browsers.
Also, some users coming through a proxy would could all share the same IP, so that doesn't necessarily protect you from ensuring the request came from the same user. I've also seen users come through proxies that might shift their IP during a session.
Ben,
A few points.
1. Do not tie the session to an IP address because IP addresses are not static over time and do not identify client computers.
2. Do not use CFMX_COMPAT - it's bogus and you might as well not be enciphering/hashing/authenticating. Use real crypto algorithms. AES-256 is a good default choice of symmetric cipher; SHA-256 is a good default choice of hash algorithm; HMAC-SHA-256 is a good default choice of authentication algorithm. These are all popular and interoperable choices, as well as being NIST recommendations.
3. You should always deal with crypto keys and cryptable/crypted data as bytes, not as text strings. You should always use CharsetDecode (http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=functions_c-d_02.html) to convert textual data to a well-known bytes representation - UTF-8 is a good default choice.
4. You should generate keys according to the block size of the cipher/hash algorithm. The block size of AES-256 is 256 bits (32 bytes); the block size of SHA-256 is 512 bits (64 bytes).
5. You should generate keys using a cryptographically-secure PRNG. You can use java.security.SecureRandom from ColdFusion.
6. You should use a salt or IV when enciphering/hashing data rather than sticking a random value in (and thereby polluting the semantics of) your data. The salt or IV should, like the key, be generated using a cryptographically-secure PRNG and should be the same number of bytes as the block size of the cipher/hash algorithm.
7. You should typically serialize a data structure's pieces in a particular order and with consistent options. For example, when serializing a data structure to JSON, serialized objects might have their keys sorted lexicographically (rather than randomly or rather than in the order that the underlying data structure's keys were created).
8. You might encode generation/expiration timestamps in UNIX-timestamp format, encoded in a url-safe base-64 variant. Be sure to use the UTC timezone rather than the local timezone of your server.
9. When comparing hashes or MACS, you should use a comparison algorithm that takes constant time (i.e., the timing of which is not affected by the data in the user-supplied hash/MAC). You may use algorithm #5 from http://rdist.root.org/2010/01/07/timing-independent-array-comparison/.
10. You should encode binary data (such as enciphered data) using a base-64 variant. The variant depends on the use case: when the base-64-encoded will be further encoded with url-encode, you might use a base-64 variant with only url-safe characters (e.g., replace + and / with - and ~ and do not pad with =). Encoding with such a base-64 variant is more compact than encoding with hex and more compact than encoding with the default base-64 variant.
11. Unless the cookie data includes sensitive data that should not be stored on the user's machine, you may use a MAC (such as HMAC-SHA-256) followed by the serialized data; you may also use a cipher. If the cookie data includes sensitive data, then you should use a cipher.
12. You should set cookies with the HTTP-only option (so that JavaScript cannot read it - although the browser will send it back in AJAX requests). If using SSL, you should also set cookies with the SSL-only option (so that the browser will not send it back on non-SSL requests).
13. You should keep production keys out of the application source code. Development/staging keys may be in the application source code. An easy solution is to keep a file named `cookies.key` or some such and include that file in every production deployment. The file will be read and its contents used as the key if the file exists; the key in the source code will be used otherwise.
14. You should rotate keys periodically. This can be a little tricky to implement while ensuring that cookies enciphered/hashed immediately before a rotation can be deciphered/authenticated immediately after it.
15. You should do all of this for *all* cookies for *all* websites.
16. If you are using multiple web servers with a load-balancer, you might store session data in a datastore such as redis (rather than polluting your RDBMS with session-data and rather than trying to fit it all in within the space available in a cookie). My preference, though, is to avoid session-state wherever possible.
Cheers,
Justice
@Justice,
All good points, but I wouldn't let "perfect" become the enemy of "good enough" though. Unless you're trying to store a credit card number in the cookie this would all probably be overkill for basic session authentication in most applications.
@Justice,
Holy cow - What a tremendous amount of awesome information there. I will admit that when it comes to security / cryptography, I am but a child and have very much to learn. I knew this, but the points that you raise make me realize just how much farther I have to go.
The whole things feels a bit overwhelming. I suppose the best thing to do would be to just get a book about cryptography and security?
@Ben -- looks like a great idea. Keeps sensitive data out of the cookie. I, like some of the other commenters, prefer just enabling session management and passing the session ID to the client. Though, even if the session ID (or any of the other data in the cookie) is encrypted or not, don't you still have the issue w/ coffee shop session spoofing (like firesheep exposed)?
I'm still a child as well in this area, but it seems to be the best (and most secure) way to manage session data is to keep it all server side and just pass a session ID to the client over https.
I'm probably just not thinking of a good use case for keeping some other data in the cookie vs server-side -- anyone have a good one?
@Mike,
If your website is served over SSL and all sensitive cookies are marked as SSL-only, then you will not have a FireSheep issue.
If your website is not served over SSL or if sensitive cookies are not marked as SSL-only, then you will have a FireSheep issue where the attacker has the ability to replay cookies. The size of the issue depends.
If sensitive cookies are timestamped and either signed (with e.g. HMAC-SHA-256) or encrypted (with e.g. AES-256), then the replay issue is somewhat mitigated. The attacker must replay the cookie before the cookie expires relative to the timestamp embedded in the cookie; if he does so, he can continue his session indefinitely; if he misses the window, the cookie becomes useless.
TL;DR
Use SSL throughout your website and mark all cookies as SSL-only.
Cheers,
Justice