Interesting Problems with .NET IsPostBack()

June 22, 2012

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.