Interesting Problems with .NET IsPostBack()

First, credit where credit is due: Bryan Jeffries (plug here for his awesome book) talked with me about this problem a couple years ago. Since then I’ve found half a dozen bugs related to IsPostBack, but I’ve never seen the potential problems written out. Thus, this post.

What does IsPostBack Do?

The MSDN article provides some insight into how most developers will treat ispostback. It says, “true if the page is being loaded in response to a client postback; otherwise, false.”

Here’s the snippet they include:

 private void Page_Load()
{
    if (!IsPostBack)
    {
        // Validate initially to force asterisks
        // to appear before the first roundtrip.
        Validate();
    }
}

How does this actually work? Below are a handful of test cases along with results.

Simple GET request, validate() is not called

GET /postback.aspx HTTP/1.1
Host: localhost:31907

Same with a simple POST, validate() is not called

POST /postback.aspx HTTP/1.1
Host: localhost:31907
Content-Type: application/x-www-form-urlencoded

a=1

Legitimate postback, as expected, validate() is called

POST /postback.aspx HTTP/1.1
Host: localhost:31907
Cookie: ASP.NET_SessionId=l1ghpm2rocgh0verdtbdaydc
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=%2FwEPDwUJNjI0NjY1NDA2D2...

Here’s the interesting thing. a POST with an empty viewstate and validate() is called

POST /postback.aspx HTTP/1.1
Host: localhost:31907
Cookie: ASP.NET_SessionId=l1ghpm2rocgh0verdtbdaydc
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

__VIEWSTATE=

Maybe even more interesting, a GET with an empty viewstate and validate() is called. So the postback doesn’t even need to be a POST!

GET /postback.aspx?__VIEWSTATE= HTTP/1.1
Host: localhost:31907

If a developer takes what MSDN says at face value – that the page is being loaded in response to a client postback – they may occasionally rely on this as a security measure without realizing it.

CSRF Vector

Let’s modify our earlier project a bit and compile it with .net 3.5

    public partial class postback : System.Web.UI.Page
    {
        protected override void OnInit(EventArgs e) 
        {
            base.OnInit(e);
            Page.ViewStateUserKey = Session.SessionID;
            
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            if(!IsPostBack)
            {
                Response.Write("Error, this page is after a post");
            }
            else
            {
                Response.Write("Ok, you're cool");
                //process
            }
        }
    }

First, note There are a lot of things //process can do. Upload files, call an operation that deletes users, etc. The one caveat for our attack is that VIEWSTATE needs to be empty. There are a lot of times this is the case with builtin .net functions. Uploading files, SQL operations, file operations, etc all just don’t require VIEWSTATE to work.

Secondly, note that VIEWSTATEUSERKEY is set to the session ID. This is generally the recommended way to protect against CSRF in .net. It’s very common for developers to set this in a master page and not think about CSRF anymore – and I agree… that’s the way it should be in an ideal world. But unfortunately it’s not always the case. In the above project, if the request is sent with an empty VIEWSTATE then //process is hit.

The root cause of this can be found in the HiddenFieldPageStatePersister Load method. Opening this with reflector, you can see that if requestViewStateString is empty then the check is bypassed:

    public override void Load()
    {
        if (base.Page.RequestValueCollection != null)
        {
            string requestViewStateString = null;
            try
            {
                requestViewStateString = base.Page.RequestViewStateString;
                if (!string.IsNullOrEmpty(requestViewStateString))
                {
                    Pair pair = (Pair) Util.DeserializeWithAssert(base.StateFormatter, requestViewStateString);
                    base.ViewState = pair.First;
                    base.ControlState = pair.Second;
                }
            }
            catch (Exception exception)
            {
                if (exception.InnerException is ViewStateException)
                {
                    throw;
                }
                ViewStateException.ThrowViewStateError(exception, requestViewStateString);
            }
        }
    }

This is hardened in ASP.net 4.0, where the if statement adds a check to see if the ViewStateUserKey is null.

 if (!string.IsNullOrEmpty(requestViewStateString) || !string.IsNullOrEmpty(base.Page.ViewStateUserKey))

So in .net 4.0, this CSRF bypass shouldn’t work as long as viewstateuserkey is set.

Auth Bypass Vector

A less common vector is when developers don’t auth their pages properly. This can be very context specific, but I found this problem in an admin application, and it had some very interesting consequences. A dumbed down version of the code is the following, which could be bypassed with an empty viewstate. Again, if the actions require VIEWSTATE and event validation, the attacker is hosed. But if this is a common construct, you’re bound to find some operations that don’t

protected void Page_Load(object sender, EventArgs e)
{
    if(!IsPostBack)
    {
        //authenticate user, show options based on auth
    }
    else
    {
        //process, an attacker can hit this without auth
    }
}

The assumption here is the same as the CSRF vector, that the “the page is being loaded in response to a client postback”. That is what MSDN says, after all. The developers are just interpreting it a certain way :) I don’t think MSDN is in the wrong here. What I do find interesting is how a non-security feature can cause a decent number of issues just because of the assumptions that people make.

Google Docs Billion Laughs

This is a writeup of a bug (now fixed) I reported to Google last year.

A billion laughs attack was present in the Google Docs document parser. When the engine would parse a document it would resolve internal entities by expanding them. This eventually earned me a spot on the Google Wall of Sheep, but alas, no reward because it’s a DoS bug and DoS bugs don’t qualify.

With any type of DoS bug on a large service, it’s fairly difficult to determine the exact severity. There might be things that mitigate this somewhat, like there is probably throttling, the APIs might restrict on a per user basis, etc. Regardless, with one request it’s likely that it could tie up a processor for some amount of time (evidence suggests at least a couple minutes, but I suspect a lot more). This was clearly a bug; Google Docs was resolving internal entities without limit which is a CPU intensive operation and requires very little client processing/bandwidth.

Here’s a link 17th order billion laughs document: test17. Unzip and look in content.xml. To increase order of attack, change the entity it points to (eg &a19;, &a20, etc). The attack itself is very straightforward, and for those who don’t want to look at the doc, it just looks something like this, generated mostly from a legitimate ODT file:

<!--?xml version="1.0" encoding="UTF-8"?-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE billion [
  <!ENTITY a0 "Bomb!">
  <!ENTITY a1 "&a0;&a0;">
  <!ENTITY a2 "&a1;&a1;">
...
  <!ENTITY a25 "&a24;&a24;">
]>
...
<text:p text:style-name="Standard">Test &a17;</text:p>

Here are some interesting metrics on various laugh sizes:

  • 16th order took about 4 seconds for the app to return
  • 17th order took about 8 seconds for the app to return
  • 18th order took about 20 seconds and the upload is eventually rejected, after several empty files are created with the same name
  • 20th order took about 1:20 and the upload is eventually rejected, dozens of empty files are created (I’m not sure what was going on with that).
  • 21st order never seemed to “finish”

One quick note is I never found anywhere that external entities were resolved. It’s hard to tell for sure because certain egress/chroot-type mitigations could have just helped make this hard to exploit. Correct or not, I sort of suspected the external entity thing might not be a problem. Openoffice resolved internal but not external entities, so (right or wrong) I guessed that it’s somewhat likely that Google is re-using or at least sort of mimicking openoffice’s document parser.

The reporting process was fairly nighmarish, but I put the blame mostly on MSVR rather than Google… not to point fingers, all I know is I never heard much of anything back.

In any serious service I think DoS bugs get a bad rap. I’ve met a lot of people who consider DoS bugs as low severity just based on that classification alone. In reality, I think people tend to care a lot more about when an online service is unavailable. When Sony did everything terribly, did most people care about the data they lost? Some did (I did), but I think most just wanted to start playing games again already. So what’s more severe? A clickjacking bug in Facebook that allows you to do a targeted attack to take over someone’s account, or a DoS bug that can bring down Facebook?

With some imagination, I wonder if something like this could have been used in the right (wrong?) hands to bring down a service as large as Google Docs.

Follow

Get every new post delivered to your Inbox.