Cutting Edge

Subclassing and Overriding ASP.NET Pages—Part II

Dino Esposito

Code download available at: Cutting Edge 2007_05.exe(163 KB)

Contents

Background
The Original Solution
Simulating a Very Simple Attack
How Does This Happen?
An Improved TextBox Control
Replacing the TextBox Control
The Definitive Solution

I recently came across a relatively large Web site that was full of extremely simple Web user controls—ASCX files, to be precise. The developers considered this approach necessary after they discovered unexpected behavior in a server control they were using.

As a result of this discovery, the developers replaced all occurrences of this server control throughout the site with a user control that contained a modified version of the original control. (At the same time, the developers also replaced numerous other controls, since they just couldn’t be sure what they would run into next.) The developers felt this extra layer of abstraction placed in between the page and the control was more reliable. In addition, there was the benefit that, if need be, a user control could easily be replaced in an ASP.NET application without touching the binaries and restarting the application. (This is not always the case, but there are deployment options that make this possible.)

I was called in to review the application and the first question I was asked was, "is there a better way to replace a server control throughout a site without extensively reworking every page?"

In my April 2007 column, I discussed a few approaches to the problem of applying limited, sometimes temporary, changes to an ASP.NET Web site without touching the original source code. This month, I explore more techniques that can be used to replace server controls and URLs in a declarative manner without the source code.

I didn’t have an immediate answer to their question, but I knew where to look for a solution. I thought that if I had developed the ASP.NET infrastructure, I would have put some sort of setting in the configuration file to let developers map tags to controls declaratively. This is not a completely new idea in ASP.NET. In ASP.NET 1.x, you can declaratively change some code-related aspects, such as the base class of Web pages and user controls. (However, this is only for pages and user controls that don’t explicitly use the Inherits clause in the Page directive.) So why not server controls, I thought. Well, my reasoning was correct: ASP.NET 2.0 provides the <tagMapping> section just for this purpose.

Background

I should share a little background. This scenario began during an internal security review when the customer discovered a security hole in an application that could lead to a classic SQL injection attack. The company applied a quick patch, but it only led to another type of problem.

In the customer’s Web site, many of the pages accept a fixed five-character code on the query string. This code is then used to compose a SQL statement. The company was still running code that looked like this:

Dim code As String = Request.QueryString(“Code”).ToString();
Dim command As String = _
    “SELECT * FROM customers WHERE id=’” & code & “’”

I sincerely hope that your sites are not still running similar code! It fully and blindly trusts any information that is passed through the query string, and it appends this information to a string that composes a SQL command. This is a serious security hazard. A knowledgeable hacker can easily find seemingly innocuous text that transforms the original and made-to-measure SQL command into a dangerous attack. If you need more details on SQL injection, start with Paul Litwin’s article "Stop SQL Injection Attacks Before They Stop You".

From a performance standpoint, sending out dynamically constructed commands is a mistake. These commands are not going to benefit from query plan reuse, since the code itself will likely be different every time they are submitted. Using a parameterized query or a stored procedure has two advantages: it ensures at least an automatic check on the type of the data being sent, and it benefits from query plan caching in SQL Server™ and other databases.

As explained in the March 2007 installment of Cutting Edge, you should validate a query string statically as you would do with the command line of a console utility. In this way, you disambiguate numbers, dates, and Boolean values, but you can’t do much when a string is expected. Here, the real problem is what’s in the string. You need some business logic to carefully validate the passed string. Verifying the length of the string, though, is a simple check that can be accomplished either through an HTTP module or with limited modification to the code.

The Original Solution

After identifying a SQL injection hole, the customer determined that limiting the size of the acceptable arguments to five characters—the real size of the code—would be a quick and effective workaround. With only five characters available, there’s not much a hacker can do against your database—at least, that was the hope. So the customer installed the HTTP module as presented in the March 2007 installment of Cutting Edge and checked the query string size of affected pages. No more than five characters were effectively sent to pages.

However, the application—which was a mix of new ASP.NET pages and revamped classic ASP pages—had some pages where the user could type the same code in a textbox and submit it. On the server, during postback, the specified code would be appended to a SQL statement using the same method illustrated earlier. Thus, the length of the text being submitted through the textbox also had to be limited. That’s an easy problem, the developers thought, and proceeded to set the MaxLength attribute of textboxes to the desired value:

<asp:textbox runat=”server” id=”TextBox1” MaxLength=”5” />

Everything appeared to be fixed. No code with more than five characters could slip into the middle tier of the site. This didn’t necessarily mean the site was safe against injections, but it certainly limited the potential for attacks. Or so they thought.

Simulating a Very Simple Attack

Consider a sample ASP.NET page like the one in Figure 1. The source code of the page is shown in Figure 2. The page features a textbox with the MaxLength attribute set to five and a submit button. When clicked, the button performs a postback and processes the contents of the textbox. Under normal circumstances, where the page is displayed in the browser, there’s really no way to type in more than five characters. If you try to paste in a longer string of text, it is cut off accordingly to the specified size.

Figure 2 Source Code of the Sample ASP.NET Page

ASPX

 
<%@ Page Language="VB" AutoEventWireup="false" 
    CodeFile="Default.aspx.vb" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="https://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Max Length</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h3>Try specifying more than 5 characters</h3>
        <asp:TextBox ID="TextBox1" MaxLength="5" 
          runat="server"></asp:TextBox><br />
        <asp:Button ID="Button1" runat="server" Text="Click" />    
    </div>
    </form>
</body>
</html>

Codebehind

Partial Class _Default : Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, _
          ByVal e As System.EventArgs) Handles Me.Load
        TextBox1.Focus()
    End Sub
    Protected Sub Button1_Click(ByVal sender As Object, _
          ByVal e As System.EventArgs) Handles Button1.Click
        Response.Write("<b>Processing text: </b>" & TextBox1.Text & "<b>
   </b><hr/>")
    End Sub
End Class

Figure 1 Sample ASP.NET Page

Figure 1** Sample ASP.NET Page **(Click the image for a larger view)

Now look at this from the attacker’s perspective. A typical attack against a Web page involves creating a copy of the page as plain HTML, altering some values, and then posting the "poisoned" version of the page. To get the HTML of the page, all the malicious user has to do is display the page as any typical user would: select View Source, then save the contents to a local HTML file. This is only possible if the attacker can physically access the page. For example, if the page is protected, the attacker must present valid credentials to view the page. However, stolen authentication cookies, phishing tactics, and other social engineering techniques can put the right information into the wrong person’s hands.

Once the markup of the ASP.NET page has been saved to the local machine, a couple of changes are required. First, the attacker must change the action attribute of the form, making it point to the absolute URL of the same ASP.NET page. Here’s the typical markup for the server form of a default.aspx ASP.NET page:

<form name=”form1” method=”post” action=”Default.aspx” id=”form1”>

The attacker would change it to the following:

<form name=”form1” method=”post” 
      action=”https://targetserver/Source/Default.aspx” id=”form1”>

There’s no action attribute on the ASP.NET HtmlForm control, but when you use plain HTML, you can still post the contents of a form to any URL you wish. The second change regards the poisoned data being posted. I’ll add a value attribute to the markup emitted by the ASP.NET TextBox server control set to a string far longer than the regular five characters:

<input name=”TextBox1” type=”text” 
maxlength=”5” id=”TextBox1” 
value=”This is a far looooonger text” />

What do you think will happen when this HTML page is displayed on the attacker’s machine and the button clicked? The result is shown in Figure 3. The address bar of the browser window on the left shows that the page being displayed is a local HTML page. And in the browser window on the right, you can see that the modified content of the form has been posted to the remote ASP.NET application. The limit of five characters has been circumvented. This shows that a malicious user has a way to send your pages text of any size regardless of the MaxLength settings on textboxes.

Figure 3 Local HTML Page that Posts Strings of Any Length

Figure 3** Local HTML Page that Posts Strings of Any Length **(Click the image for a larger view)

How Does This Happen?

You’re probably wondering where the problem lies. Is it in the browser? The ASP.NET runtime? Or perhaps the TextBox control? Yes, the real problem is in the TextBox control.

The TextBox server control doesn’t check MaxLength when a page that contains it is recreated on the server after a postback. Obviously, to be on the safe side, the TextBox should check the value of MaxLength and compare it to the length of the posted text before assigning the Text property.

An Improved TextBox Control

The TextBox, which is the server-side counterpart of the <input type=text> HTML tag, receives text that the user types into the input buffer. The TextBox needs to process this text to fire the TextChanged server event and make data available to other controls in the page. An ASP.NET control that processes posted data implements the IPostBackDataHandler interface like so:

Public Interface IPostBackDataHandler
  Function LoadPostData(ByVal postDataKey As String, _
          ByVal postCollection As NameValueCollection) As Boolean
  Sub RaisePostDataChangedEvent()
End Interface

The LoadPostData method checks whether the postback data of the TextBox control is different from its previous value and, if so, loads the content and returns true. Otherwise, it returns false.

The LoadPostData meth­od is invoked between the Init and Load events of the page, right after the content of the view state has been restored for each control. The postDataKey parameter indicates the name within the posted collection that references the content to be loaded. The postCollection parameter brings in the collection of all posted values—query string or form collection, depending on the selected HTTP verb.

The RaisePostDataChangedEvent method is invoked later in the lifecycle to raise an optional control-specific event that signals whether the state of the control changed after the postback. In practice, the method RaisePostDataChangedEvent is only invoked if LoadPostData returns true.

Figure 4 shows the pseudocode that represents the implementation of LoadPostData for the System.Web.UI.WebControls.TextBox control. Basically, the method compares the value of the Text property read from the view state with the posted value. If the two differ, the posted value replaces the current value and becomes the new value of the Text property in the control.

Figure 4 LoadPostData Method of the TextBox Control

Protected Overridable Function LoadPostData(ByVal postDataKey As?String, _
      ByVal postCollection As NameValueCollection) As Boolean
   MyBase.ValidateEvent(postDataKey)
   
   Dim oldText As String = Me.Text
   Dim newText As String = postCollection.Item(postDataKey)
   
   If (Not Me.ReadOnly AndAlso Not oldText.Equals( _
         newText, StringComparison.Ordinal)) Then
      Me.Text = newText
      Return True
   End If

   Return False
End Function

As you can see, the posted value is blindly assigned to the Text property without taking into account the length of the string. Each control can update as many properties as required in the LoadPostData method and can cross-check any properties that make sense to test. As demonstrated in Figure 4, the TextBox implementation of LoadPostData limits the verification to only ensuring that the control is not read-only and then proceeds to comparing the old and new text.

Figure 5 presents a brand new TextBox control with a slightly different implementation of the LoadPostData method. The overridden method simply cuts the posted text to the maximum allowed length before proceeding with the text comparison. As you can see in Figure 6, any text that exceeds the maximum length is automatically cut off and subsequently never used to generate further results during the postback. This happens regardless of the capabilities of the client browser.

Figure 5 Modified LoadPostData Method for TextBox

Public Class TextBox : Inherits System.Web.UI.WebControls.TextBox

    Protected Overrides Function LoadPostData( _
            ByVal postDataKey As String, _
            ByVal postCollection As NameValueCollection) As Boolean
        Page.ClientScript.ValidateEvent(Me.UniqueID, String.Empty)

        Dim oldText As String = Me.Text
        Dim newText As String = postCollection.Item(postDataKey)

        If (newText.Length > Me.MaxLength) Then
            newText = newText.Substring(0, Me.MaxLength)
        End If

        If (Not Me.ReadOnly AndAlso Not oldText.Equals( _
                newText, StringComparison.Ordinal)) Then
            Me.Text = newText
            Return True
        End If

        Return False
    End Function

End Class

Figure 6 Text Exceeding Maximum Length is Cut Off

Figure 6** Text Exceeding Maximum Length is Cut Off **(Click the image for a larger view)

If you compare the source code of the LoadPostData method in Figure 4 and Figure 5, you’ll notice a minor difference. In Figure 4, the method calls into ValidateEvent on its base class—the System.Web.UI.Control class. In Figure 5, the same code is replaced with a call to ValidateEvent on the ClientScriptMan­ager object:

Page.ClientScript.ValidateEvent(Me.UniqueID, String.Empty)

Because the ValidateEvent method on the base Control class is declared Friend (internal in C#), you cannot invoke it from any class defined outside the System.Web.dll assembly. The call stack of the ValidateEvent method on the base Control class ends up invoking the ValidateEvent method on the ClientScriptManager object; an instance of the ClientScriptManager object is exposed through the ClientScript property of the Page class.

ValidateEvent is one of the tools used to implement event validation in ASP.NET 2.0. Event validation is a built-in feature intended to help prevent page to process events (and event arguments) not specifically generated by the page and the registered control.

Replacing the TextBox Control

So now you have a brand new TextBox control. It ensures that any text exceeding the maximum length that is assigned to the Text property will be detected and removed. How do you go about using this control in your ASP.NET pages? Simply register the control with each page and replace all occurrences of the original textbox.

In ASP.NET 2.0, you can save a lot of work by adding the following configuration script to the web.config file under the <controls> section of the <pages> block:

<add tagPrefix=”x” namespace=”Samples” assembly=”TextBox” />

This ensures that all pages under the scope of the web.config file will automatically register the specified tag and controls.

But one problem still remains, and it is quite a big one. The issue is how to switch between old and new textboxes? Fortunately, with ASP.NET 2.0, you have the <tagMapping> section in the configuration file:

<pages>
   <tagMapping>
     <add tagType=”System.Web.UI.WebControls.TextBox”
          mappedTagType=”Dino.Samples.TextBox” />
   </tagMapping>
</pages>

The <tagMapping> section allows you to remap a control type onto another at compile time. This remapping causes the mapped type to be used in place of the original type for all pages and user controls in the scope of the configuration file. As per the preceding code, Dino.Samples.TextBox will now be used wherever a system TextBox is referenced. All that you have to do is write the new control and edit the web.config file. It’s surprisingly easy and quite effective.

Needless to say, the remapped type must be a class that inherits from the original type. I should also note that the ASP.NET team used this feature in pre-RTM builds of ASP.NET AJAX Extensions 1.0 to replace original validator controls with new ones that work great with the UpdatePanel control.

The Definitive Solution

The customer correctly diagnosed an issue with the ASP.NET TextBox control and its way of processing posted data. And it adequately fixed the problem by creating a new TextBox control. Since the developers didn’t know of a better, possibly declarative, way to replace controls throughout a site, they replaced all occurrences manually and wrapped them in a user control. This was to minimize the impact of any future changes.

With the tagMapping feature, the solution was a piece of cake. The technique that uses tagMapping is neat and can be applied to replace buggy controls or to add new capabilities to existing controls. Note, though, that if the remapped control has new properties or methods, you’ll need to modify the source code in order to use these new properties and methods.

(Speaking of declarative mapping, ASP.NET 2.0 also features a <urlMappings> section. It is a direct child of <configuration>. The <urlMappings> section is ASP.NET 2.0’s declarative counterpart to the RewritePath method on the HttpContext object.)

In summary, you should be aware that the original ASP.NET TextBox control doesn’t trim any posted value for the Text property when MaxLength is set. But this column should help solve the issue, providing a modified control that works around the limitation. And you can declaratively inject it in your application with a simple new line in the web.config file.

Send your questions and comments for Dino to cutting@microsoft.com.

Dino Esposito is a mentor at Solid Quality Learning and the author of Programming Microsoft ASP.NET 2.0 (Microsoft Press, 2005). Based in Italy, Dino is a frequent speaker at industry events worldwide. Get in touch with Dino at cutting@microsoft.com or join the blog at web­logs.asp.net/despos.