Bullet Proof Cookies
INTRODUCTION
Security is a hot topic these days; developers are slowly learning more and more about how to make their code more secure and how to learn defensive programming techniques. Years ago, defensive secure programming used to be a luxury, but not anymore. With the increased threats around us in the computer world, we, as developers, should keep in mind security concepts as we do our coding. One type of applications is web applications, and specifically in this article, I will be talking about ASP.NET applications. However, the ideas and concepts here can be adapted to any web programming language. You always read how cookies play an important role in the security of a web application. Cookies have several uses in web applications; for instance, ASP.NET itself uses cookies to identify a session, some sites use cookies to implement the feature of "remember me" when you log in to their site, other sites save user preferences in cookies. I am going to talk briefly about cookies and what makes them vulnerable to attacks. I will give some examples of how cookies can be abused, and finally, I will talk about what we need to do to make our cookies bullet proof to defeat each one of the vulnerabilities.
USING THE CODE
Let us start by talking about the problems cookies have and give a small example on each problem. Cookies have their contents in clear text, which means the end user can see the contents of the cookies stored on the machine. Some cookies save user information like username and password; if a cracker puts his hand on your cookies, then you are in trouble, he might get a hold of your login credentials and hijack your accounts. Crackers can sniff your cookies from the network, or they can steal the cookies by physical access to your machine or by having a spyware installed on your machine. The answer to this problem, already a common knowledge, is to encrypt the contents of your cookies.
Another problem with cookies is that your web application is trusting the input of the cookies blindly. I once had an account on a certain site, where when you choose the option of "Keep me logged in", they send you a cookie with your account ID in it. The account ID was a sequential integer number; I edited the cookie on my machine and put another number in it, and I opened the site again and there it was, I had access to a totally different account. I could manipulate this cookie to enable me to gain access to every account they had on their system, this is called account hopping. What made this worse is that if you log in into a different account and try to edit your information, the password was sent in clear text to you! To solve this problem, you need a way to make sure that the cookies you issue are not changed or modified. Someone might ask here, but if the cookies are encrypted, wouldn't this solve the problem? How can someone manipulate a cookie if it is encrypted? Maybe the hacker somehow managed to discover your encrypting/decryption key, by carefully analyzing your encryption patterns. The discussion of this topic is beyond the scope of this article. Just know that it is a possibility and the cracker would be able to manipulate your cookies. So, we need a better solution, digital signatures.
Yet another problem with cookies is that when you issue a cookie and give it an expiry date, you are trusting the browser to stop sending you the cookie after the set date; not only so, the cookie could be edited on the client end of the application and its expiry date could be easily changed. One nice add-on for Firefox is a cookie editor, it allows you to do all these things. Suppose a site sends you a cookie with a unique identifier to keep you logged in, the application issues the cookie for 1 week to make sure it will not be saved forever. Even if the cracker cannot do account hopping on the cookie, but if the identifier is bound to a user account, then this cookie can be used to log in into the account forever by manipulating the expiry date of the cookie. Even if the end user changes his password, it will not help. To solve this problem, we need to put our own absolute expiry date in the cookie content itself and protect it from being changed by stopping the cookie from being manipulated.
Cookies are also used to bind a client to a session on the server. Since HTTP has no way of identifying a connection uniquely, it has to send the session cookies (along with other cookies) on each request. If a cracker gets a hold of your session cookie (by sniffing packets over the network or by installing a spyware), then he will be able to open the same session that you have open on your browser. This security problem is called session hijacking. Once he hijacks your session, he could change your password and lock you out of your account, or he could have access to sensitive information like banking information.
Keep in mind that even if a cookie is encrypted and the cracker cannot read its contents, well, he does not have to read its contents to steal it and send it from his own machine to potentially gain access to your account. So, we also need a way to bind a cookie to the client's machine as uniquely as possible to make it much harder for the cracker to simply take the cookie to his own browser and gain access that he should not have.
To recap, we need to make sure that cookies are not saved in clear text by encrypting them. We need to make sure that the cookies are not manipulated and that they are indeed issued by us by digitally signing them. We need to make sure that when we design our cookies with a certain expiry date in mind, we should be able to trust the expiry date. We do this by adding the date to the cookie contents itself. Cookies can be stolen by the cracker and manually put on his machine so he can have access to your accounts, so we need to try to bind the cookie to the client's machine as uniquely as possible.
Let us talk a little more about the idea of binding a cookie to a client's machine to make it difficult to be stolen to another browser on another machine. The easiest and simplest method is to bind the cookie to the IP address of the client's machine. This is not a perfect solution as it is still vulnerable to man in the middle attacks and also if the cracker is on the same physical network of the user and they are using one IP address to connect to the internet (via a proxy or a NAT server); then, binding to the IP address will not be enough as both requests will seem to be coming from the same IP. I experimented with another method which makes the binding very tight. I used the server variable "REMOTE_PORT" which gives you the port number of the client's machine that the connection is opened on; however, this solution is not perfect because it is still vulnerable to man in the middle attacks, but is it much harder for the cracker to take on this cookie. The usage of "REMOTE_PORT" is possible only when the server is using HTTP keep alive and only when the browser has one window opened with the end site. Also, the standard keep alive time is 5 minutes, so the connection will be dropped after 5 minutes of inactivity. Of course, this parameter can be changed. As I said, I experimented with this method. I cannot say it is a perfect solution for many reasons.
First of all, if the user wants to open another window, then the new window will force the browser to open another connection, which will effectively make your application drop the session. The browser is not forced to keep the connection alive, it is just a preference. The browser could close the connection and open another one. If the end user is using a proxy server, the proxy server might choose to close connections to save on resources, so again, this will affectively drop the session. Depending on the strength of the security you want, you might want to consider binding to the "REMOTE_PORT". If I was asked to write a private banking application, I will choose this method. The security benefits outweigh the inconveniences; however, if you are creating a community website, for example, I am sure your end users will be annoyed to be thrown out of the site when they open another window or if they are behind a proxy server. You can choose any number of server variables to bind to, things like the client's user agent; however, I cannot think of one variable that a cracker cannot duplicate. Try to experiment with other server variables to bind to.
What about SSL and Secure Cookies? SSL and secure cookies will solve the problem of someone sniffing your cookies over the network. However, SSL is not the perfect solution for securing your cookies. There are ways to make SSL vulnerable to man in the middle attacks; however, it is not 100% undetectable. Also, when you mark a cookie as being secure, this does not mean that the cookie will be encrypted on the end user's machine. It is up to the browser to decide how to store it, so a cracker having physical access to your machine can still take advantage of such cookies. It is always a good idea to set SSL and mark your cookies as secure if you can. It will protect you from a family of problems. The best method of coding is to understand each defensive technique, what it does, what it solves, and then you combine methods as you see fit for your application.
Let us get our hands dirty. Let us start by encrypting our cookies. ASP.NET provides a handful of symmetric encryption algorithms to cover most of our needs. I will be using the
RijndaelManaged
class to encrypt our cookies because it is both powerful and fast. I will write a simple function that will take a string input and give us a string output of the same data encrypted. Keep in mind that each application you develop should have its own key.
Hide Shrink Copy Code
// must create your own keys and ivs
private static byte[] key_192 = new byte[]
{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
private static byte[] iv_128= new byte[]
{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10};
public static string EncryptRijndaelManaged(string value)
{
if (value == "")
return "";
RijndaelManaged crypto = new RijndaelManaged();
MemoryStream ms = new MemoryStream();
CryptoStream cs = new CryptoStream(ms, crypto.CreateEncryptor(key_192, iv_128),
CryptoStreamMode.Write);
StreamWriter sw = new StreamWriter(cs);
sw.Write(value);
sw.Flush();
cs.FlushFinalBlock();
ms.Flush();
return Convert.ToBase64String(ms.GetBuffer(), 0, (int) ms.Length);
}
Also, we will create a function that will decrypt the value encrypted and return to us the clear text.
Hide Copy Code
public static string DecryptRijndaelManaged(string value)
{
if (value == "")
return "";
RijndaelManaged crypto = new RijndaelManaged();
MemoryStream ms = new MemoryStream(Convert.FromBase64String(value));
CryptoStream cs = new CryptoStream(ms, crypto.CreateDecryptor(key_192, iv_128),
CryptoStreamMode.Read);
StreamReader sw = new StreamReader(cs);
return sw.ReadToEnd();
}
The above code covers our encryption needs, it is straightforward. You should change the keys for each different application. Next, we will have to write a function that will digitally sign the contents of the cookies. To digitally sign data, we need to use an asymmetric algorithm. Of course, we will use the famous RSA algorithm (public-private keys). In short, to sign a given data, we need to hash the data with a certain hashing algorithm and then sign this hash with the private key of the algorithm. When we want to verify the signed data, we decrypt the encrypted hash with the public key of the algorithm and compare it with a hash that we calculate on the data that we get. I also want to be able to combine several variables in the same cookie. I will use a simple XML structure to combine several data within one cookie; for example, the original cookie contents as one piece of data, the expiry date another piece of data, and possibly other information like the IP address of the host. I will write a function that will take several pieces of information, combine them (via an XML structure), sign them, encrypt them, and return the data as one single string to facilitate putting it in a cookie; and symmetrically, I will write a function to take this string, decrypt it, verify the decrypted data from not being changed, and finally separate the combined pieces of information into their original parts.
Hide Copy Code
public static string SignAndSecureData(string value)
{
return SignAndSecureData(new string[] {value});
}
public static string SignAndSecureData(string[] values)
{
string xmlKey = "MUST ADD YOUR OWN DEFAULT XML RSA KEY HERE";
return SignAndSecureData(xmlKey, values);
}
public static string SignAndSecureData(string xmlKey, string[] values)
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml("<x></x>");
for (int i = 0; i < values.Length; i++)
_AddNode(xmlDoc, "v" + i.ToString(), values[i]);
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.FromXmlString(xmlKey);
byte[] signature = rsa.SignData(Encoding.ASCII.GetBytes(xmlDoc.InnerXml),
"SHA1");
_AddNode(xmlDoc, "s", Convert.ToBase64String(signature, 0, signature.Length));
return EncryptRijndaelManaged(xmlDoc.InnerXml);
}
And below, the code that does the decryption, verification, and separation of values:
Hide Shrink Copy Code
public static bool DecryptAndVerifyData(string input, out string[] values)
{
string xmlKey = "MUST ADD YOUR OWN DEFAULT XML RSA KEY HERE";
return DecryptAndVerifyData(xmlKey, input, out values);
}
public static bool DecryptAndVerifyData(string xmlKey,
string input, out string[] values)
{
string xml = DecryptRijndaelManaged(input);
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xml);
values = null;
XmlNode node = xmlDoc.GetElementsByTagName("s")[0];
node.ParentNode.RemoveChild(node);
// verify
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.FromXmlString(xmlKey);
byte[] signature = Convert.FromBase64String(node.InnerText);
byte[] data = Encoding.ASCII.GetBytes(xmlDoc.InnerXml);
if (!rsa.VerifyData(data, "SHA1", signature))
return false;
// count values
int count;
for (count = 0; count < 100; count++)
{
if (xmlDoc.GetElementsByTagName("v" + count.ToString())[0] == null)
break;
}
values = new string[count];
for (int i = 0; i < count; i++)
values[i] = xmlDoc.GetElementsByTagName("v" + i.ToString())[0].InnerText;
return true;
}
Please note that to get an XML RSA key, you can simply create an object of
RSACryptoServiceProvider
and call ToXmlString
to get the key (include private key for signing).
Note that at this stage, you have two very powerful functions
SignAndSecureData
,DecryptAndVerifyData
that can be used in a lot of places in your code to encrypt and sign data. I will now write two functions that will make use of these two methods to sign and secure cookies and symmetrically decrypt and verify cookies.
Hide Shrink Copy Code
public static void SignAndSecureCookie(HttpCookie cookie, NameValueCollection serverVariables)
{
if (cookie.HasKeys)
throw (new Exception("Does not support cookies with sub keys"));
if (cookie.Expires != DateTime.MinValue) // has an expiry date
{
cookie.Value = SignAndSecureData(new string[]
{cookie.Value,
serverVariables["REMOTE_ADDR"],
cookie.Expires.ToString()});
}
else
{
cookie.Value = SignAndSecureData(new string[]
{cookie.Value, serverVariables["REMOTE_ADDR"]});
}
}
public static string DecryptAndVerifyCookie(HttpCookie cookie,
NameValueCollection serverVariables)
{
if (cookie == null)
return null;
string[] values;
if (!DecryptAndVerifyData(cookie.Value, out values))
return null;
if (values.Length == 3) // 3 values, has an expiry date
{
DateTime expireDate = DateTime.Parse(values[2]);
if (expireDate < DateTime.Now)
return null;
}
if (values[1] != serverVariables["REMOTE_ADDR"])
return null;
return values[0];
}
At this point, we now have the code at hand to do all the tasks that we want to secure our cookies.
Let us see how to use this code to do two common tasks. The first task is to secure our session cookies in a better way. The way we will do this is to override/implement three functions in the Global.aspx file. The first function is
Session_Start
which will be used to create another cookie by us that will have the session ID in it. However, our cookie is special in that it will be signed, to prevent it from being modified, and it will be bound to the IP address of the end user. This will help make it very difficult to hijack your sessions. If you want to make the binding tighter, you might want to consider the idea I gave above in this article, binding the cookie to the REMOTE_PORT as well. The second function we will implement is an event handler for PreRequestHandlerExecute
. In this function, we will check the session cookie ID against the ID that we stored in our signed cookie; if they do not match, then we will abandon the session. Finally, we will implement another function Session_End
to help wnsure that the browser will delete our signed cookie. Here is the code for all this; note that this code goes in the Global.aspx file; two functions are already there and one event for which you have to add the event handler function.
Hide Copy Code
protected void Session_Start(Object sender, EventArgs e)
{
System.Web.HttpCookie cookie =
new System.Web.HttpCookie("__signed_session", Session.SessionID);
CHelperMethods.SignAndSecureCookie(cookie, Request.ServerVariables);
Response.Cookies.Add(cookie);
}
private void Global_PreRequestHandlerExecute(object sender, System.EventArgs e)
{
if (CHelperMethods.DecryptAndVerifyCookie(Request.Cookies["__signed_session"],
Request.ServerVariables) != Session.SessionID)
{
Session.Abandon();
Response.Redirect("http:// YOUR MAIN PAGE HERE");
}
}
protected void Session_End(Object sender, EventArgs e)
{
Response.Cookies["__signed_session"].Expires = DateTime.Now.AddDays(-1);
}
The second common task that I would like to show you is how to add code to easily enable the usage of secure cookies in your web application. In my web applications, I have a common class that I use as a base class for all my pages. I urge you to do the same as it will prove useful in the future. For example, in this same base class that I use, I have functions to obfuscate email addresses by injecting JavaScript code to hide the email addresses. This will prevent spiders harvesting emails from your websites. You can check the code in the attached zip file. You can also download a free tool I have onmy website called "Coder's TextObfuscator" that will do this and more for several programming languages.
I will create two protected functions that you will be able to use in all your pages;
RequestSecureCookies
and ResponseSecureCookies
. One function will simply allow you to add a cookie to your response, and the other will return the cookie for you (from the request) if the cookie is valid. You can check the code in the attached zip file.A WORD OF CAUTION...
Like everything else in programming, there is a trade off. Before you go ahead and start using this code in all your applications, remember that there is an increased processing overhead that comes from encrypting/signing/decrypting/verifying code. Use the secure cookies wisely, add it to the sensitive cookies only, and do not overuse it. Remember that cookies are sent on each request. This means that on each request, there is an added processing of decryption/verification. Also, there is the slight increase in size of the encrypted cookie.
CONCLUSION
I hope my ideas and code here will help you to do better, more secure coding. Please feel free to ask me any questions. Use the code wisely and remember that there is no perfect solution, but you can make things much harder for crackers out there. I will take a moment to remind you of Client Certificates, a feature I have never seen used by any website. If you need the highest levels of security, besides using the ideas presented in this article, you might want to consider implementing Client Certificates. Happy secure coding...
0 comments:
Post a Comment