Effectively managing user state in Web applications can be a tricky balancing act of performance, scalability, maintainability and security. The security consideration is especially evident when you’re managing user state stored on the client. I have a colleague who used to say that handing state data to a client is like handing an ice cream cone to a 5-year-old: you may get it back, but you definitely can’t expect to get it back in the same shape it was when you gave it out!
In this month’s column, we’ll examine some security implications around client-side state management in ASP.NET applications; specifically, we’re going to look at view state security. (Please note: this article assumes that you’re familiar with the concept of ASP.NET view state. If not, check out “Understanding ASP.NET View State” by Scott Mitchell).
If you don’t think there’s any data stored in your applications’ view state worth protecting, think again. Sensitive information can find its way into view state without you even realizing it. And even if you’re vigilant about preventing sensitive information loss through view state, an attacker can still tamper with that view state and cause even bigger problems for you and your users. Luckily, ASP.NET has some built-in defenses against these attacks. Let’s take a look at how these defenses can be used correctly.
At Microsoft, development teams use the STRIDE model to classify threats. STRIDE is a mnemonic that stands for:
The main two STRIDE categories of concern from the view state security perspective are Information Disclosure and Tampering (although a successful tampering attack can lead to a possible Elevation of Privilege; we’ll discuss that in more detail later). Information disclosure is the simpler of these threats to explain, so we’ll discuss that first.
One of the most unfortunately persistent misconceptions around view state is that it is encrypted or somehow unreadable by the user. After all, a view state string certainly doesn’t look decomposable:
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwULLTE2MTY2ODcyMjkPFgIeCHBhc3N3b3JkBQlzd29yZGZpc2hkZA==" />
However, this string is merely base64-encoded, not encrypted with any kind of cryptographically strong algorithm. We can easily decode and deserialize this string using the limited object serialization (LOS) formatter class System.Web.UI.LosFormatter:
LosFormatter formatter = new LosFormatter();
object viewstateObj = formatter.Deserialize("/wEPDwULLTE2MTY2ODcyMjkPFgIeCHBhc3N3b3JkBQlzd29yZGZpc2hkZA==");
A quick peek in the debugger (see Figure 1) reveals that the deserialized view state object is actually a series of System.Web.UI.Pair objects ending with a System.Web.UI.IndexedString object with a value of “password” and a corresponding string value of “swordfish.”
Figure 1 Secret View State Data Revealed by the Debugger
If you don’t want to go to the trouble of writing your own code to deserialize view state objects, there are several good view state
decoders available for free download on the Internet, including Fritz Onion’s ViewState Decoder tool available at alt.pluralsight.com/tools.aspx.
In “The Security Development Lifecycle: SDL: A Process for Developing Demonstrably More Secure Software” (Microsoft Press, 2006), Michael Howard and Steve Lipner discuss technologies that can be used to mitigate STRIDE threats. Figure 2 shows threat types and their associated mitigation techniques.
Figure 2 Techniques to Mitigate STRIDE Threats
Because we’re dealing with an information disclosure threat to our data stored in the view state, we need to apply a confidentiality mitigation technique; the most effective confidentiality mitigation technology in this case is encryption.
ASP.NET version 2.0 has a built-in feature to enable encryption of view state—the ViewStateEncryptionMode property, which can be enabled either through a page directive or in the application’s web.config file:
<%@ Page ViewStateEncryptionMode="Always" %>
There are three possible values for ViewStateEncryptionMode: Always (the view state is always encrypted); Never (the view state is never encrypted); and Auto (the view state is only encrypted if one of the page’s controls explicitly requests it). The Always and Never values are pretty self-explanatory, but Auto requires a little more explanation.
If a server control persists sensitive information into its page’s view state, the control can request that the page encrypt the view state by calling the Page.RegisterRequiresViewStateEncryption method (note that in this case the entire view state is encrypted, not just the view state corresponding to the control that requested it):
public class MyServerControl : WebControl
protected override void OnInit(EventArgs e)
However, there is a caveat. The reason the method is named RegisterRequiresViewStateEncryption, and not something like EnableViewStateEncryption, is because the page can choose to ignore the request. If the page’s ViewStateEncryptionMode is set to Auto (or Always), the control’s request will be granted and the view state will be encrypted. If ViewStateEncryptionMode is set to Never, the control’s request will be ignored and the view state will be unprotected.
This is definitely something to be aware of if you’re a control developer. You should consider keeping potentially sensitive information out of the view state (which is always a good idea). In extreme cases where this isn’t possible, you might consider overriding the control’s SaveViewState and LoadViewState methods to manually encrypt and decrypt the view state there.
In a single-server environment, it’s sufficient just to enable ViewStateEncryptionMode, but in a server farm environment there’s some additional work to do. Symmetric encryption algorithms—like the ones that ASP.NET uses to encrypt the view state—require a key. You can either explicitly specify a key in the web.config file, or ASP.NET can automatically generate a key for you. Again, in a single-server environment it’s fine to let the framework handle key generation, but this won’t work for a server farm. Each server will generate its own unique key, and requests that get load
balanced between different servers will fail because the decryption keys won’t match.
You can explicitly set both the cryptographic algorithm and the key to use in the machineKey element of your application’s web.config file:
<machineKey decryption="AES" decryptionKey="143a...">
For the encryption algorithm, you can choose AES (the default value), DES or 3DES. Of these, DES is explicitly banned by the Microsoft SDL Cryptographic Standards, and 3DES is strongly discouraged. I recommend that you stick with AES for maximum security.
Once you’ve selected an algorithm, you need to create a key. However, remember that the strength of this system’s security depends on the strength of that key. Don’t use your pet’s name, your significant other’s birthday or any other easily guessable value! You need to use a cryptographically strong random number. Here’s a code snippet to create one in the format that the machineKey element expects (hexadecimal characters only) using the .NET RNGCryptoServiceProvider class:
RNGCryptoServiceProvider csp = new RNGCryptoServiceProvider();
byte data = new byte;
string value = String.Join("", BitConverter.ToString(data).Split('-'));
At a minimum, you should generate 16-byte random values for your keys; this is the minimum value allowed by the SDL Cryptographic Standards. The maximum length supported for AES keys is 24 bytes (48 hex chars) in the Microsoft .NET Framework 3.5 and earlier, and 32 bytes (64 hex chars) in the .NET Framework 4. DES supports a maximum key length of only 8 bytes and 3DES a maximum of 24 bytes, regardless of the framework version. Again, I recommend that you avoid these algorithms and use AES instead.
Tampering is the other significant threat. You might think the same encryption defense that keeps attackers from prying into the view state would also prevent them from changing it, but this is wrong. Encryption doesn’t provide defense against tampering: Even with encrypted data, it’s still possible for an attacker to flip bits in the encrypted data.
Take another look at Figure 2. To mitigate a tampering threat, we need to use a data integrity technology. The best choice here is still a form of cryptography, and it’s still built into ASP.NET, but instead of using a symmetric algorithm to encrypt the data, we’ll use a hash algorithm to create a message authentication code (MAC) for the data.
The ASP.NET feature to apply a MAC is called EnableViewStateMac, and just like ViewStateEncryptionMode, you can apply it either through a page directive or through the application’s web.config file:
<%@ Page EnableViewStateMac="true" %>
To understand what EnableViewStateMac is really doing under the covers, let’s first take a high-level look at how view state is written to the page when view state MAC is not enabled:
When view state MAC is enabled, there are three additional steps that take place between the previous steps 2 and 3:
Whenever this page is posted back to the server, the page code validates the incoming __VIEWSTATE by taking the incoming state graph data (deserialized from the __VIEWSTATE value), adding the same secret key value, and recomputing the hash value. If the new recomputed hash value matches the hash value supplied at the end of the incoming __VIEWSTATE, the view state is considered valid and processing proceeds (see Figure 3). Otherwise, the view state is considered to have been tampered with and an exception is thrown.
Figure 3 Applying a Message Authentication Code (MAC)
The security of this system lies in the secrecy of the secret key value. This value is always stored on the server, either in memory or in a configuration file (more on this later)—it is never written to the page. Without knowing the key, there would be no way for an attacker to compute a valid view state hash.
Theoretically, with enough computing power an attacker could reverse-engineer the key: He has knowledge of a computed hash value and knowledge of the corresponding plaintext, and there aren’t too many options available for the hash algorithm. He would only have to cycle through all the possible key values, re-compute the hash for the known plaintext plus the current key and compare it to the known hash. Once the values match, he knows he’s found the correct key and can now attack the system at will. The only problem with this is the sheer number of possible values: The default key size is 512 bits, which means there are 2 to the power of 512 different possibilities, which is so large a number that a brute force attack is completely unfeasible.
The default value of EnableViewStateMac is true, so protecting your applications is as simple as not setting it to false. Unfortunately, there is some misleading documentation concerning the performance impact of EnableViewStateMac, and some Web sites are encouraging developers to disable view state MAC in order to improve the performance of their applications. Even the MSDN online documentation for PagesSection.EnableViewStateMacProperty is guilty of this, stating: “Do not set EnableViewStateMac to true if performance is a key consideration.” Do not follow this advice! (Hopefully, by the time you’re reading this, the documentation will have been changed to better reflect security considerations.)
Any page that has its view state MAC-disabled is potentially vulnerable to a cross-site scripting attack against the __VIEWSTATE parameter. The first proof-of-concept of this attack was developed by David Byrne of Trustwave, and demonstrated by Byrne and his colleague Rohini Sulatycki at the Black Hat DC conference in February 2010. To execute this attack, the attacker crafts a view state graph where the malicious script code he wants to execute is set as the persisted value of the innerHtml property of the page’s form element. In XML form, this view state graph would look something like Figure 4.
Figure 4 XML Code for View State MAC Attack
<String>...malicious script goes here...</String>
The attacker then base-64 encodes the malicious view state and appends this string as the value of a __VIEWSTATE query string parameter for the vulnerable page. For example, if the page home.aspx on the site www.contoso.com was known to have view state MAC disabled, the attack URI would be http://www.contoso.com/home.aspx?__VIEWSTATE=/w143a...
All that remains is to trick a potential victim into following this link. Then the page code will deserialize the view state from the incoming __VIEWSTATE query string parameter and write the malicious script as the innerHtml of the form. When the victim gets the page, the attacker’s script will immediately execute in the victim’s browser, with the victim’s credentials.
This attack is especially dangerous because it completely bypasses all of the usual cross-site scripting (XSS) defenses. The XSS Filter in Internet Explorer 8 will not block it. The ValidateRequest feature of ASP.NET will block several common XSS attack vectors, but it does not deserialize and analyze incoming view state, so it’s also no help in this situation. The Microsoft Anti-Cross Site Scripting (Anti-XSS) Library (now included as part of the Microsoft Web Protection Library) is even more effective against XSS than ValidateRequest; however, neither the Anti-XSS Library input sanitization features nor its output encoding features will protect against this attack either. The only real defense is to ensure that view state MAC is consistently applied to all pages.
Similar to ViewStateEncryptionMode, there are special considerations with EnableViewStateMac when deploying applications in a server farm environment. The secret value used for the view state hash must be constant across all machines in the farm, or the view state validation will fail.
You can specify both the validation key and the HMAC algorithm to use in the same location where you specify the view state encryption key and algorithm—the machineKey element of the web.config file:
<machineKey validation="AES" validationKey="143a...">
If your application is built on the .NET Framework 3.5 or earlier, you can choose SHA1 (the default value), AES, MD5 or 3DES as the MAC algorithm. If you’re running .NET Framework 4, you can also choose MACs from the SHA-2 family: HMACSHA256, HMACSHA384 or HMACSHA512. Of these choices, MD5 is explicitly banned by the SDL Crypto Standards and 3DES is strongly discouraged. SHA1 is also discouraged, but for .NET Framework 3.5 and earlier applications it’s your best option. .NET Framework 4 applications should definitely be configured with either HMACSHA512 or HMACSHA256 as the validation algorithm.
After you choose a MAC algorithm, you’ll also need to manually specify the validation key. Remember to use cryptographically strong random numbers: if necessary, you can refer to the key generation code specified earlier. You should use at least 128-byte validation keys for either HMACSHA384 or HMACSHA512, and at least 64-byte keys for any other algorithm.
Unlike a vulnerable file permission or database command that may be hidden deep in the server-side code, vulnerable view state is easy to find just by looking for it. If an attacker wanted to test a page to see whether its view state was protected, he could simply make a request for that page himself and pull the base-64 encoded view state value from the __VIEWSTATE form value. If the LosFormatter class can successfully deserialize that value, then it has not been encrypted. It’s a little trickier—but not much—to determine whether view state MAC has been applied.
The MAC is always applied to the end of the view state value, and since hash sizes are constant for any given hash algorithm, it’s fairly easy to determine whether a MAC is present. If HMACSHA512 has been used, the MAC will be 64 bytes; if HMACSHA384 has been used, it will be 48 bytes, and if any other algorithm has been used it will be 32 bytes. If you strip 32, 48 or 64 bytes off of the end of the base-64 decoded view state value, and any of these deserialize with LosFormatter into the same object as before, then view state MAC has been applied. If none of these trimmed view state byte arrays will successfully deserialize, then view state MAC hasn’t been applied and the page is vulnerable.
Casaba Security makes a free tool for developers called Watcher that can help automate this testing. Watcher is a plug-in for Eric Lawrence’s Fiddler Web debugging proxy tool, and it works by passively analyzing the HTTP traffic that flows through the proxy. It will flag any potentially vulnerable resources that pass through—for example, an .aspx page with a __VIEWSTATE missing a MAC. If you’re not already using both Fiddler and Watcher as part of your testing process, I highly recommend giving them a try.
View state security is nothing to take lightly, especially considering the new view state tampering attacks that have recently been demonstrated. I encourage you to take advantage of the ViewStateEncryptionMode and EnableViewStateMac security mechanisms built into ASP.NET.
Bryan Sullivan is a security program manager for the Microsoft Security Development Lifecycle team, where he specializes in Web application security issues. He’s the author of “Ajax Security” (Addison-Wesley, 2007).
Thanks to the following technical expert for reviewing this article: Michael Howard
Hey Bryan, interesting read. I have a problem with our WSS 3.0 site that puts out a "viewstate" error all of a sudden. I want it gone first, then -maybe- reapply it. It's a nuisance.
More MSDN Magazine Blog entries >
Browse All MSDN Magazines
Subscribe to MSDN Flash newsletter
Receive the MSDN Flash e-mail newsletter every other week, with news and information personalized to your interests and areas of focus.