Tuesday, November 20, 2007

PersistentStatePage with Event Validation

Few months ago I've came upon a great article "Persisting the state of a web page" by Martin Jericho.

The idea is very cool and useful - the viewstate of a web page can be stored on the server-side and restored on demand. Please refer to the article for further explanations if you are not familiar with Martin's solution.

As a kind of a hack, the solution is not perfect. There are few small problems with the implementation (for example, I've added ".ToLower()" in few places where addresses are compared) but the main issue with Martin's approach is connected with the ASP.NET event validation mechanism.

You see, to reduce the risk of cross-site request forgery attack, ASP.NET is rather paranoic when validating the input coming from the browser. Specifically, the validation signature is placed in the hidden __EVENTVALIDATION field and the signature is used to validate if the request is vaild.

Since in Martin's solution the viewstate is restored from the external resource, the event validation must be turned off, otherwise you get the "Invalid postback or callback argument" exception. However, turning off the event validation is a huge security risk.

I belive I have found a way to have the event validation turned on and still be able to use Martin's solution.

What's the problem, doc?

First of all, why it does not work with event validation turned on?

It seems that this just does not work:


   1: private void LoadPostData(Control control, ArrayList modifiedControls) {
   2: {
   3:   ..
   4:   // Call the framework's LoadPostData on this control using the name 
   5:   // attribute as the post data key:
   6:   if (((IPostBackDataHandler)control).LoadPostData(nameAttribute,PostData))
   7:       modifiedControls.Add(control);

The LoadPostData method, implemented internally in the .NET library, does the validation and just does not accept the external viewstate provided upon the state retrieval.

Solution? Well, almost

If we just were able to disable the validation only for these request which involve the persisted viewstate and keep validation on for all other valid requests... Unfortunately, it seems that the EnableEventValidation property is another paranoiac - if you see the inner implementation


   1: public virtual void set_EnableEventValidation(bool value)
   2: {
   3:     if (base.ControlState > ControlState.FrameworkInitialized)
   4:     {
   5:         throw new InvalidOperationException( ... );
   6:     }
   7:     this._enableEventValidation = value;
   8: }

then you will realise that the property is somewhat special - it cannot be switched always on your demand but rather before the engine initializes.

Few experiments reveal that the constructor is a good place to switch validation on/off and this is what Martin does - he turns the validation off in the constructor.

However, yet another place where you are allowed to turn the validation on/off is the DeterminePostBackMode method. The new implementation would be:


   1: protected override NameValueCollection DeterminePostBackMode() {
   2:     pageState=LoadPageState(Request.Url);
   4:     // this line turns the validation off but only when the state
   5:     // is actually restored. otherwise the validation should remain
   6:     // turned on.
   7:     if ( IsRestoredPageState ) EnableEventValidation = false;
   9:     NameValueCollection normalReturnObject = base.DeterminePostBackMode();

What's interesting is that after this small enhancement, the code works as expected. Well, almost.

Yet another issue

It looks like turning the validation off does in fact two things. Not only it prevents the validation of the request on the server side but also prevents the __EVENTVALIDATION hidden field to be appended to the response.

This makes the above solution only partially succesfull - even though the restored page is accepted on the server side, since the response does not contain the validation signature, the page will likely fail on another postback! The engine will just see that the validation is turned on (it is, by default), there is no state to restore (it has been restored one postback ago) but the __EVENTVALIDATION is missing (since the validation was turned off last time the page had been processed on the server). Guess what? You will get the "Invalid postback or callback"!

What we need is then not only to turn the validation off before the state is retrieved but also to turn it on after it is retrieved, so that the __EVENTVALIDATION signature is appended to the response!

But how do we enable the validation if the EnableEventValiation property cannot be altered after the state is retrieved?

Well, using ... reflection.

I know, the hack is dirty but it works. It seems that the EnableEventValidation property is just a wrapper on the _enableEventProperty internal boolean field. So just after the state is retrieved, we just turn on the validation:


   1: override protected void OnLoad(EventArgs e) {
   2:     // this is Martin's code
   3:     if (IsRestoredPageState) {
   4:         // Populate controls with PostData, saving a list of those that were modified:
   5:         ArrayList modifiedControls=new ArrayList();
   6:         LoadPostData(this,modifiedControls);
   7:         // Raise PostDataChanged event on all modified controls:
   8:         foreach (IPostBackDataHandler control in modifiedControls)
   9:             control.RaisePostDataChangedEvent();
  11:         // and this is my dirty hack which turns the validation on
  12:         // after the state is retrieved so that the
  13:         // __EVENTVALIDATION is correctly appended to the response
  14:         FieldInfo fi = typeof(Page)
  15:            .GetField( "_enableEventValidation", 
  16:                 BindingFlags.NonPublic | BindingFlags.Instance );
  17:         if ( fi != null )
  18:             fi.SetValue( this, true );
  19:     }
  20:     base.OnLoad(e);
  21: }

Well, this is all. Two small modifications, the first one in the DeterminePostbackMode and second one in the OnLoad. It works correctly in my test application, please feel free to share your experiences.


Anonymous said...

I've seen a situation where this doesn't work but setting EnableEventValidation to false in the ctor works okay.

The problem occurs when using the browsers back button and opening the same page twice with different arguments.

Anonymous said...

I use a lot of controls that "Autopostback" to server. This causes the "Page has expired" warning message to appear if a user clicks the browser back button. Also refreshing the page in certain situations causes some previous action to re-occur.

The original article on codeproject is about persisting a page to return to later. My needs required me to persist a page and return to it immediately through a Response.Redirect() so the browser won't display the page expiration message.

I think I have succeeded in doing so following the methods posted on codeproject and without the need for turning off EventValidation. Several changes are required but the most important one is in DeterminePostBack() where if PageState.PostData is null then we return base.DeterminePostBack() else we return PageState.PostData. The original article says to return Request.Forms() and manually invoke the RaisePostBackEvent() on each modified control. Returning PageState.PostData causes the framework to take care of all that automatically without raising an invalid callback or postback argument error.

Hope this helps.

Wiktor Zychla said...

that's quite interesting news. did you publish your changes somewhere so that anyone could pick the modified source?

Unknown said...

Does anyone have an algorythm or recommendation that allows you to use server side storage to manage the event validation post variable? In a similar fashion to Jeff & Jon or Martin? This would be handy for a busy application which causes the user to post large __EVENTVALIDATION and __VIEWSTATE vars.


lluthus said...

Dear Wiktor Zychla,

how I can fix this problem ?

there is a problem with checkboxes that has the autopostback attribute set to true.
If the user handles that checked event and relies on other stuff on the page ( that hasn't been loaded yet) it may be too early for the checkbox, thus resulting in invalid references...

So some workaround has to be made so that the checkbox's check event is not fired in LoadPost data.

Any thoughts?

Best Regards