asp.net: __EVENTVALIDATION is related to page virtual path...

--- #Summary: ---

When using performance test tools to record asp.net pages for test, and later those pages were moved to different path at the same server, not only have to change the recorded test scripts to the correct paths, but also need to capture and replace the new path's "__EVENTVALIDATION" field if the pages were requested with HTTP POST. 

--- #Introduction: ---

In my project I am in charging of doing performance test using Empirix's E-Test Suite. E-Test scripts was recorded and customized for each testing asp.net page and the performance test was going well until something happened the other days.

Due to project requirement some of the asp.net pages were moved to different paths under the same server. although the pages were not modified at all, and I did modified the url path of pages in E-Test scripts, those asp.net pages that E-Test recorded with previous paths were still not working anymore and throwing exceptions as follows:

"The viewstate is invalid for this page and might be corrupted"

and the error target was at form post field "__EVENTVALIDATION".

--- #Solution: ---

The "__EVENTVALIDATION" field was encoded / encrypted using a modifier that's calculated and related to page's virtual path. thus when a page was changed to different path, the modifier will also changes and if submit the page using original "__EVENTVALIDATION" field that's generated using previous page, the event validation check will fail and throw exceptions like above.

To solve this, when pages changed paths, not only have to modify the test scripts to the correct paths, but also need to get the new paths' "__EVENTVALIDATION" field to replace the scripts' ones.

--- #More Information: ---

By looking into System.Web.dll, if Event Validation was set enabled, Page object will call ClientScriptManager object to save the event validation content to "__EVENTVALIDATION" hidden field. that's where the event validation content was generated.

    1: // under System.Web.UI.Page class...
    2: internal void EndFormRender(HtmlTextWriter writer, string formUniqueID)
    3: {
    4:     // ...
    5:  
    6:     if (this.EnableEventValidation)
    7:     {
    8:         this.ClientScript.SaveEventValidationField(); 
    9:     }
   10:  
   11:     // ...
   12: }
    1: // System.Web.UI.ClientScriptManager.SaveEventValidationField() code.
    2: internal void SaveEventValidationField()
    3: {
    4:     string eventValidationFieldValue = this.GetEventValidationFieldValue(); 
    5:     if (!string.IsNullOrEmpty(eventValidationFieldValue))
    6:     {
    7:         this.RegisterHiddenField("__EVENTVALIDATION", eventValidationFieldValue);
    8:     }
    9: }
    1: // System.Web.UI.ClientScriptManager.GetEventValidationFieldValue() code
    2: internal string GetEventValidationFieldValue()
    3: {
    4:     if ((this._validEventReferences != null) && (this._validEventReferences.Count != 0))
    5:     {
    6:         return this._owner.CreateStateFormatter().Serialize(this._validEventReferences);
    7:     }
    8:     return string.Empty;
    9: }

CreateStateFormatter() will return a System.Web.UI.ObjectStateFormatter object to serialize event validation content.

    1: // System.Web.UI.ObjectStateFormatter.Serialize() code.
    2: public string Serialize(object stateGraph)
    3: {
    4:     string str = null;
    5:     MemoryStream memoryStream = GetMemoryStream();
    6:     try
    7:     {
    8:         this.Serialize(memoryStream, stateGraph);
    9:         memoryStream.SetLength(memoryStream.Position);
   10:         byte[] buf = memoryStream.GetBuffer();
   11:         int length = (int) memoryStream.Length;
   12:         if ((this._page != null) && this._page.RequiresViewStateEncryptionInternal)
   13:         {
   14:             buf = MachineKeySection.EncryptOrDecryptData(true, buf, this.GetMacKeyModifier() , 0, length);
   15:             length = buf.Length;
   16:         }
   17:         else if (((this._page != null) && this._page.EnableViewStateMac) || (this._macKeyBytes != null))
   18:         {
   19:             buf = MachineKeySection.GetEncodedData(buf, this.GetMacKeyModifier() , 0, ref length);
   20:         }
   21:         str = Convert.ToBase64String(buf, 0, length);
   22:     }
   23:     finally
   24:     {
   25:         ReleaseMemoryStream(memoryStream);
   26:     }
   27:     return str;
   28: }

Both EncryptOrDecryptData() and GetEncodedDate() use GetMacKeyModifier() to form the crypto modifier.

    1: // System.Web.UI.ObjectStateFormatter.GetMacKeyModifier()
    2: private byte[] GetMacKeyModifier()
    3: {
    4:     if (this._macKeyBytes == null)
    5:     {
    6:         if (this._page == null)
    7:         {
    8:             return null;
    9:         }
   10:         
   11:         //Rex: the num variable used page.TemplateSourceDirectory to get hashcode.
   12:         int num = StringComparer.InvariantCultureIgnoreCase.GetHashCode(this._page.TemplateSourceDirectory)  + StringComparer.InvariantCultureIgnoreCase.GetHashCode(this._page.GetType().Name);
   13:         string viewStateUserKey = this._page.ViewStateUserKey;
   14:         if (viewStateUserKey != null)
   15:         {
   16:             int byteCount = Encoding.Unicode.GetByteCount(viewStateUserKey);
   17:             this._macKeyBytes = new byte[byteCount + 4];
   18:             Encoding.Unicode.GetBytes(viewStateUserKey, 0, viewStateUserKey.Length, this._macKeyBytes, 4);
   19:         }
   20:         else
   21:         {
   22:             this._macKeyBytes = new byte[4];
   23:         }
   24:  
   25:         // Rex: use num variable to form the modifier...
   26:         this._macKeyBytes[0] = (byte) num;
   27:         this._macKeyBytes[1] = (byte) (num >> 8);
   28:         this._macKeyBytes[2] = (byte) (num >> 0x10);
   29:         this._macKeyBytes[3] = (byte) (num >> 0x18);
   30:     }
   31:     return this._macKeyBytes;
   32: }

_Page.TemplateSourceDirectory() return page's virtual directory path.

    1: // System.Web.UI.Control.TemplateSourceDirectory property
    2: public virtual string TemplateSourceDirectory
    3: {
    4:     get
    5:     {
    6:         if (this.TemplateControlVirtualDirectory == null)
    7:         {
    8:             return string.Empty;
    9:         }
   10:         return this.TemplateControlVirtualDirectory.VirtualPathStringNoTrailingSlash; 
   11:     }
   12: }

finally, EncryptOrDecryptData() form the string of "__EVENTVALIDATION", also append the modifier in the end of the string. if it's on decryption mode, it verifies the modifier to see if postback-ed modifier matched currently calculated one (which is related to page url path), if page url changed, the modifier will also change, and then match operation will fail and throw exception.

    1: // System.Web.Configuration.MachineKeySection.EncryptOrDecryptData() code
    2: internal static byte[] EncryptOrDecryptData(bool fEncrypt, byte[] buf, byte[] modifier, int start, int length, bool useValidationSymAlgo)
    3: {
    4:     EnsureConfig();
    5:     MemoryStream stream = new MemoryStream();
    6:     ICryptoTransform cryptoTransform = GetCryptoTransform(fEncrypt, useValidationSymAlgo);
    7:     CryptoStream stream2 = new CryptoStream(stream, cryptoTransform, CryptoStreamMode.Write);
    8:     stream2.Write(buf, start, length);
    9:     if (fEncrypt && (modifier != null))
   10:     {
   11:         // Rex: Append the modifier to the end of content
   12:         stream2.Write(modifier, 0, modifier.Length);
   13:     }
   14:     stream2.FlushFinalBlock();
   15:     byte[] src = stream.ToArray();
   16:     stream2.Close();
   17:     ReturnCryptoTransform(fEncrypt, cryptoTransform, useValidationSymAlgo);
   18:     if ((fEncrypt || (modifier == null)) || (modifier.Length <= 0))
   19:     {
   20:         return src;
   21:     }
   22:     for (int i = 0; i < modifier.Length; i++)
   23:     {
   24:         // Rex: compare the modifier to see if it matches...
   25:         // Rex: modifier is related to page url path.
   26:         // Rex: if url changed, the match will fail the exception will be thrown...
   27:         if (src[(src.Length - modifier.Length) + i] != modifier[i])
   28:         {
   29:             throw new HttpException(SR.GetString("Unable_to_validate_data"));
   30:         }
   31:     }
   32:     byte[] dst = new byte[src.Length - modifier.Length];
   33:     Buffer.BlockCopy(src, 0, dst, 0, dst.Length);
   34:     return dst;
   35: }

This concluded that "__EVENTVALIDATION" field is related to page url path.

--- #End ---

Technorati Tags: microsoft,asp.net,viewstate,event,validation,programming