When using a TextBox for data entry a risk is that users may not enter necessary or appropriate data for script processing. Suppose, for instance, a script performs some type of arithmetic processing on entered data. If the user fails to enter a data value or fails to enter a numeric value, a script error occurs, aborting page processing and causing embarassment for the programmer. It is always a good idea, then, to validate user input prior to activating a script that processes that input data.
There are several ways to validate user input depending on the type of data needed for script processing. The most common requirement is for the mere existence of an input value. Some value, any value, is expected in the TextBox before a script can carry out its processing. In other cases, an input value must be of a particular data type. A numeric value must be entered if arithmetic processing is to occur, or a value within a particular range of values may be expected.
For common types of data validation, ASP.NET provides a set of validation controls. These validators can test for missing values, comparison values, values within a range, and other forms of data to ensure that proper data is supplied to processing scripts. These controls are associated with TextBox controls and perform their tests automatically when Button, LinkButton, or ImageButton controls are clicked to call subprograms for processing. If a validation test is not met, the validator displays an error message to call attention to this fact, and the user is given the chance to reenter valid data in the associated TextBox.
The <asp:RequiredFieldValidator> Control
The most common type of validation test is for a missing value. The <asp:RequiredFieldValidator> control tests a TextBox for a missing value and issues an error message if this is the case. Importantly, the subprogram that processes the TextBox value is not activated by its button click until data is entered into the TextBox. The general format for a RequiredFieldValidator is shown below.
<asp:RequiredFieldValidator id="id" Runat="Server" ControlToValidate="controlID" Display="Dynamic|None|Static" ErrorMessage="string" SetFocusOnError="False|True" ValidationGroup="name" />
An id is required only if the validator is referenced in a script. The ControlToValidate property gives the id of the TextBox to which the validation test is applied. ErrorMessage supplies a text and XHTML string to format an error message. SetFocusOnErrorMessage places a blinking text cursor in the associated TextBox for ease in entering a value. ValidationGroup identifies a grouping of TextBox controls to which a set of validation tests applies when different buttons activate different sets of tests.
The RequiredFieldValidator occupies horizontal space on the page equal to the length of the error message. Often, this error-message space appears along side the TextBox to which it applies. The space is preallocated on the page unless Display="Dynamic" is coded for the validator. In this case, no message space is preallocated; it is produced dynamically when an error message is displayed.
Use of a RequiredFieldValidator is shown below. Click the button without entering data into the TextBox. A message appears at the location of the validator, and a blinking cursor is placed in the TextBox for entering a data value. Then enter any data in the TextBox and click the button.
<SCRIPT Runat="Server"> Sub Get_Data (Src As Object, Args As EventArgs) Output.Text = "You entered '" & MyTextBox.Text & "'" End Sub </SCRIPT> <form Runat="Server"> <asp:TextBox id="MyTextBox" Runat="Server"/> <asp:Button Text="Submit" OnClick="Get_Data" Runat="Server"/> <asp:RequiredFieldValidator Runat="Server" ControlToValidate="MyTextBox" ErrorMessage="Please enter a data value" Display="Dynamic" SetFocusOnError="True"/> <asp:Label id="Output" Runat="Server"/> </form>
When the button is clicked, the RequiredFieldValidator is automatically applied to the TextBox prior to calling the Get_Data subprogram. If the TextBox is empty, an error message is displayed and the cursor is placed back in the TextBox. If a data value appears in the TextBox, the validation test is passed and the subprogram is called to display the entered value. The validator is coded with Display="Dynamic" so that it does not take up space on the page if no error message is produced. This permits the Label message to appear along side the TextBox, occupying the same space as the error message.
The <asp:RangeValidator> Control
The <asp:RangeValidator> control tests a TextBox for a value within the range of two values, inclusively. The test can be made for Currency (can include a dollar sign and commas), Date, Double (floating-point), Integer, or String (default) data types.
<asp:RangeValidator id="id" Runat="Server" ControlToValidate="controlID" Display="Dynamic|None|Static" ErrorMessage="string" MaximumValue="value" MinimumValue="value" SetFocusOnError="False|True" Type="Currency|Date|Double|Integer|String" ValidationGroup="name" />
The entered value is treated as a string if a different type is not given by the Type property. An empty TextBox is evaluated as a valid data type; therefore, a RequiredFieldValidator normally needs to be paired with the RangeValidator to ensure that missing data is not treated as valid.
Use of a RangeValidator is shown below. It is paired with a RequiredFieldValidator so that missing data is not accepted as valid. First, click the button without entering data into the TextBox; then enter a number outside the range 0 - 9.
<SCRIPT Runat="Server"> Sub Get_Data (Src As Object, Args As EventArgs) Output.Text = "You entered '" & MyTextBox.Text & "'" End Sub </SCRIPT> <form Runat="Server"> Enter a value between 0 and 9:<br/> <asp:TextBox id="MyTextBox" Runat="Server"/> <asp:Button Text="Submit" OnClick="Get_Data" Runat="Server"/> <asp:RangeValidator Runat="Server" ControlToValidate="MyTextBox" Type="Integer" MinimumValue="0" MaximumValue="9" ErrorMessage="Please enter an integer in the range 0 to 9" Display="Dynamic" SetFocusOnError="True"/> <asp:RequiredFieldValidator Runat="Server" ControlToValidate="MyTextBox" ErrorMessage="Please enter a data value" Display="Dynamic" SetFocusOnError="True"/> <asp:Label id="Output" Runat="Server"/> </form>
Notice that both validators have the property setting Display="Dynamic". The result is that both error messages occupy the same space on the page rather than appearing side by side.
The <asp:CompareValidator> Control
The <asp:CompareValidator> control tests a TextBox for a value within the range of two values, inclusively. The test can be made for Currency (can include a dollar sign and commas), Date, Double (floating-point), Integer, or String (default) data types.
<asp:CompareValidator id="id" Runat="Server" ControlToCompare="controlID" ControlToValidate="controlID" Display="Dynamic|None|Static" ErrorMessage="string" Operator="Equal|NotEqual|GreaterThan|GreaterThanEqual|LessThan| LessThanEqual|DataTypeCheck" SetFocusOnError="False|True" Type="Currency|Date|Double|Integer|String" ValidationGroup="name" ValueToCompare="value" />
The value against which the TextBox value is compared can be a literal value (given by the ValueToCompare property) or it can be the value contained in another control on the page (given by the ControlToCompare property). By default, the comparison is for equality; however, other types of comparisons are made by coding the Operator property. By selecting the DataTypeCheck operator a test is made for a comparable data type to that given by the Type property. In this case, any ControlToCompare or ValueToCompare property setting is ignored.
An empty TextBox is evaluated as a valid comparison; therefore, a RequiredFieldValidator normally needs to be paired with the CompareValidator to ensure that missing data is not treated as valid. Use of a CompareValidator is shown below. It is paired with a RequiredFieldValidator so that missing data is not accepted as valid. Enter a negative number to display the error message.
<SCRIPT Runat="Server"> Sub Get_Data (Src As Object, Args As EventArgs) Output.Text = "You entered '" & MyTextBox.Text & "'" End Sub </SCRIPT> <form Runat="Server"> Enter a positive number:<br/> <asp:TextBox id="MyTextBox" Runat="Server"/> <asp:Button Text="Submit" OnClick="Get_Data" Runat="Server"/> <asp:CompareValidator Runat="Server" ControlToValidate="MyTextBox" ValueToCompare="0" Type="Double" Operator="GreaterThan" ErrorMessage="Please enter a number greater than 0" Display="Dynamic" SetFocusOnError="True"/> <asp:RequiredFieldValidator Runat="Server" ControlToValidate="MyTextBox" ErrorMessage="Please enter a data value" Display="Dynamic" SetFocusOnError="True"/> <asp:Label id="Output" Runat="Server"/> </form>
The comparison is for a floating-point value (Type="Double") greater than 0 (ValueToCompare="0"; Operator="GreaterThan").
The <asp:CustomValidator> Control
You may have validation tests that cannot be performed with a combination of RequiredFieldValidator, RangeValidator, and CompareValidator. The <asp:CustomValidator> is available for these additional tests. Its general format is shown below.
<asp:CustomValidator id="id" Runat="Server" ControlToValidate="controlID" Display="Dynamic|None|Static" ErrorMessage="string" SetFocusOnError="False|True" ValidationGroup="name" OnServerValidate="subprogram" />
The control's properties are similar to other validation controls except that it includes the OnServerValidate property to call a subprogram to perform explicitly coded tests. Again, an empty TextBox is considered valid, so a RequiredFieldValidator normally needs to be paired with the CustomValidator to test for a missing value prior to performing custom tests.
The called subprogram has the special ServerValidateEventArgs argument. Its IsValid property is set to False to indicate failure of a validation test. The argument's Value property is a reference to the value of the TextBox identified in the control's ControlToValidate property.
Use of a CustomValidator is shown below. It is paired with a RequiredFieldValidator so that missing data is not accepted as valid. A valid data value is an integer between 0 and 99.
<SCRIPT Runat="Server"> Sub Validate_TextBox (Src As Object, Args As ServerValidateEventArgs) If Not IsNumeric(Args.Value) Then MyValidator.ErrorMessage = "Please enter a number" Args.IsValid = False Else If Not Args.Value Mod 1 = 0 Then MyValidator.ErrorMessage = "Please enter an integer" Args.IsValid = False End If If Args.Value < 0 Then MyValidator.ErrorMessage = "Please enter a positive integer" Args.IsValid = False End If If Args.Value > 99 Then MyValidator.ErrorMessage = "Please enter a positive integer " & _ "between 0 and 99" Args.IsValid = False End If End If End Sub Sub Get_Data (Src As Object, Args As EventArgs) If Page.IsValid Then Output.Text = "You entered '" & MyTextBox.Text & "'" End If End Sub </SCRIPT> <form Runat="Server"> Enter a positive integer:<br/> <asp:TextBox id="MyTextBox" Runat="Server"/> <asp:Button Text="Submit" OnClick="Get_Data" Runat="Server"/> <asp:CustomValidator id="MyValidator" Runat="Server" ControlToValidate="MyTextBox" Display="Dynamic" SetFocusOnError="True" OnServerValidate="Validate_TextBox"/> <asp:RequiredFieldValidator Runat="Server" ControlToValidate="MyTextBox" ErrorMessage="Please enter a data value" Display="Dynamic" SetFocusOnError="True"/> <asp:Label id="Output" Runat="Server"/> </form>
When the "Submit" button is clicked to call the Get_Data subprogram, validation is first performed. The RequiredFieldValidator checks for any entered value; then, subprogram Validate_TextBox is called by the CustomValidator's OnServerValidate property.
This subprogram performs various checks on the entered value, referenced here as Args.Value through the subprogram's argument list. Tests are made for a numeric value, for an integer value, for a positive value, and for a value in the range 0 - 99. If any of these tests fail, the Args.IsValid property is set to false to indicate there is a validation failure on the page.
After the validation tests are performed, the Get_Data subprogram is run. Recall that this is the subprogram initially called by the button click but delayed until after validation tests are performed. Processing in this subprogram is contingent upon all validation tests being passed; therefore, it is surrounded by the Page.IsValid condition, which is false if Args.IsValid="False" is set in the validation subprogram.
Validation Groups
It is likely that two or more TextBoxes are provided for data entry to a processing script. Also, different groupings of TextBoxes might be associated with different form submission buttons and require validation independently of other groups of controls. By associating a validation control with a particular ValidationGroup, more control can be assumed over which TextBoxes are validated when particular buttons are clicked.
The following data entry form comprises a validation group that is associated with the "Submit" button. Only those TextBoxes whose validation controls are a member of this group are validated when the button is clicked.
<SCRIPT Runat="Server"> Sub Validate_Email (Src As Object, Args As ServerValidateEventArgs) If InStr(Args.Value, "@") = 0 Then EmailValidator.ErrorMessage = "Please enter a valid email address" Args.IsValid = False End If End Sub Sub Get_Data (Src As Object, Args As EventArgs) If Page.IsValid Then Output.Text = "You entered valid data" End If End Sub </SCRIPT> <form Runat="Server"> <h3>Data Entry Form</h3> <table border="0" cellpadding="2"> <tr> <td>First Name:</td> <td><asp:TextBox id="FirstName" Runat="Server"/></td> <td><asp:RequiredFieldValidator Runat="Server" ControlToValidate="FirstName" ValidationGroup="Group1" ErrorMessage="Please enter your first name" Display="Dynamic"/></td> </tr> <tr> <td>Last Name:</td> <td><asp:TextBox id="LastName" Runat="Server"/></td> <td><asp:RequiredFieldValidator Runat="Server" ControlToValidate="LastName" ValidationGroup="Group1" ErrorMessage="Please enter your last name" Display="Dynamic"/></td> </tr> <tr> <td>Age:</td> <td><asp:TextBox id="Age" Width="50" Runat="Server"/></td> <td><asp:RangeValidator Runat="Server" ControlToValidate="Age" ValidationGroup="Group1" Type="Integer" MinimumValue="15" MaximumValue="99" ErrorMessage="Please enter a value between 15 and 99" Display="Dynamic"/> <asp:RequiredFieldValidator Runat="Server" ControlToValidate="Age" ValidationGroup="Group1" ErrorMessage="Please enter your age" Display="Dynamic"/></td> </tr> <tr> <td>Email:</td> <td><asp:TextBox id="Email" Runat="Server"/></td> <td><asp:CustomValidator id="EmailValidator" Runat="Server" ControlToValidate="Email" ValidationGroup="Group1" Display="Dynamic" OnServerValidate="Validate_Email"/> <asp:RequiredFieldValidator Runat="Server" ControlToValidate="Email" ValidationGroup="Group1" ErrorMessage="Please enter your email address" Display="Dynamic"/></td> </tr> </table> <br/> <asp:Button Text="Submit" ValidationGroup="Group1" OnClick="Get_Data" Runat="Server"/> <asp:Label id="Output" Runat="Server"/><br/> </form>
A validation group is created by assigning a ValidationGroup name to the button that triggers validation. Then, only those controls which are to be validated on this button click are assigned the same ValidationGroup name. If there are other validation controls on the page, they are not validated when the button is clicked, nor does the click of a different button cause validation of the grouped controls.
The <asp:ValidationSummary> Control
By coding an <asp:ValidationSummary> control on the page individual error messages generated by separate controls can be displayed together. This summary control also can limit its error reporting to an identified set of grouped controls. The general format for a ValidationSummary is presented below.
<asp:ValidationSummary id="id" Runat="Server" DisplayMode="BulletList|List|SingleParagraph" HeaderText="string" ShowMessageBox="False|True" ShowSummary="False|True" ValidationGroup="name" />
The DisplayMode of the error summary is a bulleted list unless a simple List or SingleParagraph is specified. The report displays at the page location of the ValidationSummary control. Instead, it can be displayed as a pop-up message box by coding ShowMessageBox="True"; its on-page display is suppressed with ShowSummary="False". If the summary report pertains to a particular validation group, that group is named in the ValidationGroup property.
The following example is a repeat of the previous form with a ValidationSummary displayed instead of individual validation messages.
Scripting is identical to the previous example. Changes to individual validation controls include changing the Display mode to "None" so that error messages do not display next to the TextBoxes but only in the summary control. The form floats to the left of the page so the ValidationSummary appears to its right.
<form Runat="Server"> <h3>Data Entry Form</h3> <div style="float:left; margin-right:20px"> <table border="0" cellpadding="2"> <tr> <td>First Name:</td> <td><asp:TextBox id="FirstName" Runat="Server"/></td> <td><asp:RequiredFieldValidator Runat="Server" ControlToValidate="FirstName" ValidationGroup="Group1" ErrorMessage="Please enter your first name" Display="None"/></td> </tr> <tr> <td>Last Name:</td> <td><asp:TextBox id="LastName" Runat="Server"/></td> <td><asp:RequiredFieldValidator Runat="Server" ControlToValidate="LastName" ValidationGroup="Group1" ErrorMessage="Please enter your last name" Display="None"/></td> </tr> <tr> <td>Age:</td> <td><asp:TextBox id="Age" Width="50" Runat="Server"/></td> <td><asp:RangeValidator Runat="Server" ControlToValidate="Age" ValidationGroup="Group1" Type="Integer" MinimumValue="15" MaximumValue="99" ErrorMessage="Please enter a value between 15 and 99" Display="None"/> <asp:RequiredFieldValidator Runat="Server" ControlToValidate="Age" ValidationGroup="Group1" ErrorMessage="Please enter your age" Display="None"/></td> </tr> <tr> <td>Email:</td> <td><asp:TextBox id="Email" Runat="Server"/></td> <td><asp:CustomValidator id="EmailValidator" Runat="Server" ControlToValidate="Email" ValidationGroup="Group1" Display="None" OnServerValidate="Validate_Email"/> <asp:RequiredFieldValidator Runat="Server" ControlToValidate="Email" ValidationGroup="Group1" ErrorMessage="Please enter your email address" Display="None"/></td> </tr> </table> <br/> <asp:Button Text="Submit" ValidationGroup="Group1" OnClick="Get_Data" Runat="Server"/> <asp:Label id="Output" Runat="Server"/><br/> </div> <asp:ValidationSummary Runat="Server" ValidationGroup="Group1" DisplayMode="BulletList" HeaderText="Please enter values for the following fields:" ShowSummary="True" ShowMessageBox="False"/>
The ValidationSummary can appear in a pop-up message box (a JavaScript alert() box) by changing the ShowMessageBox property to "True".
Using Parameterized Queries
Any time TextBox controls are used to solicit user input that is applied against a database, there is vulnerability that invalid data can abort execution of scripts or accidentally corrupted the database. The validation controls are a first line of defense against corruptable data, helping to ensure that only the correct form of data of the correct types enter the system.
There is a more serious problem, though, to take into account. TextBoxes can be the unwitting sources of intentionally malicious code that can easily destroy an entire database. Although the assumption is that TextBoxes are for entering data into the system, the reality is they can be used to enter code.
SQL Injections
Suppose you have provided a TextBox to enter a data value for matching against a database to retrieve the corresponding record. Within a script, the entered value is concatenated within an SQL SELECT statement in the form
SQLString = "SELECT * FROM MyTable WHERE MyKey = '" & MyTextBox.Text & "'"
The SELECT statement is dynamically composed in the script to insert the entered value, and all is well. However, consider the consequences of someone entering the following SQL code into the TextBox.
Now, the query that is composed by the script becomes
SELECT * FROM MyTable WHERE MyKey = ''; DROP DATABASE MyDatabase
The expected data value becomes a null value to complete the query, and a second SQL statement deletes the entire database. An SQL injection has occured that substitutes innocent data with malicious code. Although multiple SQL statements per line are not permitted for an Access database, other database management systems can execute such code.
The validation controls can help block these kinds of code insertions by testing the length of the input string, testing the entered data type, or testing for a data value within an expected range. However, even these tests may not offer sufficient protection when the expected value itself is an extended text string.
Parameterized SQL Queries
The best way to protect from malicious code entering the system is by using parameters, rather than data values, to compose SQL queries. Parameters are placeholders for data values, and they ensure that values are treated as literal values rather than as code. Consider the following subprogram that builds a SELECT statement to retrieve a record from the Books table of the BooksDB.mdb database. The statement is assigned as the SelectCommand property of an AccessDataSource (id="BookSource") associated with a GridView to display the record.
Sub Get_Record (Src As Object, Args As EventArgs) Dim SQLString As String SQLString = "SELECT BookID, BookTitle, BookPrice FROM Books " & _ "WHERE BookID = @BookID" BookSource.SelectCommand = SQLString End Sub
A parameter is identified by a name preceded by the "@" character. This parameter@BookID in this exampleis a reference to a data value that is inserted into the SELECT statement to retrieve the matching record. Assume that this value comes from a TextBox as in the following example.
The value entered into the TextBox becomes the value of parameter @BookID in the SELECT statement. So, it is necessary to explicitly associate the TextBox with this parameter. When using an AccessDataSource to retrieve database information, parameters for a SELECT statement are identified in the <SelectParameters> section of the control. This section includes one or more <asp:ControlParameter> controls associating a named parameter with the control that supplies its value. Code for the AccessDataSource used in the above example along with the TextBox that supplies the @BookID value is shown below.
Book ID: <asp:TextBox id="BookIDInput" Runat="Server"/> <asp:Button Text="Find" OnClick="Get_Record" Runat="Server"/> <asp:AccessDataSource id="MySource" Runat="Server" DataFile="../Databases/BooksDB.mdb"> <SelectParameters> <asp:ControlParameter Name="BookID" ControlID="BookIDInput" PropertyName="Text"/> </SelectParameters> </asp:AccessDataSource>
A ControlParameter includes three properties to associate a SELECT parameter with the control supplying its value. The Name property is a parameter name appearing in the SELECT statement (without the preceding "@"); ControlID is the id of the control that is the source of the parameter value; PropertyName is the property of the control that references this value (the Text property for a TextBox).
In this example, there is only one parameter appearing in the SELECT statement, therefore the need for only a single ControlParameter. Multiple parameters in a SELECT statement would necessitate multiple <asp:ControlParameter> controls, one for each SELECT parameter. Their order of appearance is unimportant. Notice that since a SelectParameters section is included inside the AccessDataSource it requires both an opening and closing control to encompass the section.
Code Simplification
Remember, a parameter inserted in a SELECT statement is treated as a literal data value. Therefore, unlike when using string concatenations to compose the statement, malicious code is effectively defused. It is treated as data rather than code. A secondary advantage of using parameters is that it reduces the complications in coding the statement. It is not necessary to string together literal code interspersed with control references and apostrophes surrounding string data. For instance, a SELECT statement that would be composed of concatenated string values like the following,
SQLString = "SELECT * FROM Books WHERE " & _ "BookType = '" & TypeTextBox.Text & "' AND " & _ "BookPrice > " & PriceTextBox.Text & " AND " & _ "BookQty <= " & QtyTextBox.Text & " AND " & _ "BookTitle LIKE '%" & TitleTextBox.Text & "%'"
can be simplified by substituting parameters,
SQLString = SELECT * FROM Books WHERE " & _ "BookType = @BookType AND BookPrice > @BookPrice AND " & _ "BookQty <= @BookQty AND BookTitle LIKE @BookQty"
and remembering to include a SelectParameters sections inside the AccessDataSource to identify the controls supplying the values for the parameters.
In the remaining tutorials, both coding techniques are used for instructional purposes. However, it is strongly recommended that parameterized SQL statements be used for all production code where TextBoxes supply values for searching against a database.