Validating User Input in Razor Pages

When you allow users to provide values that you then process, you need to ensure that the incoming values are of the expected data type, that they are within the permitted range and that required values are present. This process is known as input validation.

The term "user input" covers any value that the user has control over. Values provided via forms constitute the bulk of user input, but user input also comes in the form of values provided in URLs and cookies. The default position should be that all user input is to be considered untrusted.

You can validate user input in two places in a web application: in the browser using client-side script or the browser's in-built data type validation; and on the server. However, you should only ever view client-side validation as a courtesy to the user because it is easily circumnavigated.

The MVC framework, on which Razor Pages is built, includes a robust validation framework that works against inbound model properties on the client-side and on the server.

The key players in the input validation framework are:

  • DataAnnotation Attributes
  • Tag Helpers
  • jQuery Unobtrusive Validation
  • ModelState
  • Route Constraints

DataAnnotation Attributes

The primary building block of the validation framework is a set of attributes that inherit from ValidationAttribute. Most of these attributes reside in the System.ComponentModel.DataAnnotations namespace.

Attribute Description
Compare1 Used to specify another form field that the value should be compared to for equality
MaxLength Sets the maximum number of characters/bytes/items that can be accepted
MinLength Sets the minimum number of characters/bytes/items that can be accepted
PageRemote2 Enables client-side validation against a server-side resource, such as a database check to see if a username is already in use
Range Sets the minimum and maximum values of a range
RegularExpression Checks the value against the specified regular expression
Remote2 Enables client-side validation against a server-side resource, such as a database check to see if a username is already in use
Required Specifies that a value must be provided for this property. Note that non-nullable value types such as DateTime and numeric values are treated as required by default and do not need this attribute applied to them
StringLength Sets the maximum, and optionally, the minimum number of string characters allowed

Notes

  1. The Compare attribute does not work as expected when applied to properties of a PageModel in Razor Pages 3.x and earlier. The workaround is to either manually compare the property values in code, or to create a create a "wrapper" object for the bound properties (like an InputModel). The Compare attribute is supported when it is applied to the properties of such an object. For further discussion, see this GitHub issue. From .NET 5 onwards, the Compare attribute works without any additional steps being necessary.
  2. The Remote attribute works within the context of an MVC controller. The PageRemote attribute is designed for use in remote validation of PageModel properties in Razor Pages.

Apart from the Remote attribute, all the other attributes cause validation to occur on both the client and the server. The Remote attribute also differs from the others in that it doesn't belong to the DataAnnotations namespace. It is found in the Microsoft.AspNetCore.Mvc namespace.

Attributes are applied to properties on the inbound model - typically a PageModel or ViewModel:

public class UserModel : PageModel
{
    [BindProperty]
    [Required]
    [MinLength(6)]
    public string UserName { get; set; }

    [BindProperty]
    [Required, MinLength(6)]
    public string Password { get; set; }

    [BindProperty, Required, Compare(nameof(Password))]
    public string Password2 { get; set; }

    ...

Each attribute can be declared separately, or as a comma separated list, or a mixture of both.

Client side validation

Important

Client-side validation should only ever be viewed as a courtesy to users, in that it provides immediate feedback to the user in the event that they have not provided satisfactory input. Your application must not rely solely on client-side validation because it is very easy to circumvent by anyone who has a small amount of HTML/JavaScript knowledge.

Client-side validation support is provided by the jQuery Unobtrusive Validation library, developed by Microsoft. You must include jQuery Unobtrusive Validation within the page containing the form for client side validation to work. This is most easily accomplished by the inclusion of the _ValidationScriptsPartial.cshtml file (located in the Shared folder) within the page:

@section scripts{
   <partial name="_ValidationScriptsPartial" />
}

Obviously, you should also ensure that jQuery is available to the page too.

Client side validation works with special HTML5 data-* attributes emitted by tag helpers. To see how that works, here is a simple tag helper-based form featuring the properties above:

<form method="post">
    <div>
        <input asp-for="UserName" />
        <span asp-validation-for="UserName"></span>
    </div>
    <div>
        <input asp-for="Password" />
        <span asp-validation-for="Password"></span>
    </div>
    <div>
        <input asp-for="Password2" />
        <span asp-validation-for="Password2"></span>
    </div>
    <div>
        <input type="submit" />
    </div>
</form>

This form uses the validation message tag helper to output validation error messages. This is how the form renders as HTML:

<div>
    <input type="text" data-val="true" data-val-minlength="The field UserName must be a string or array type with a minimum length of &#x27;6&#x27;." data-val-minlength-min="6" data-val-required="The UserName field is required." id="UserName" name="UserName" value="" />
    <span class="field-validation-valid" data-valmsg-for="UserName" data-valmsg-replace="true"></span>
<div>
    <input type="text" data-val="true" data-val-minlength="The field Password must be a string or array type with a minimum length of &#x27;6&#x27;." data-val-minlength-min="6" data-val-required="The Password field is required." id="Password" name="Password" value="" />
    <span class="field-validation-valid" data-valmsg-for="Password" data-valmsg-replace="true"></span>
</div>
<div>
    <input type="text" data-val="true" data-val-equalto="&#x27;Password2&#x27; and &#x27;Password&#x27; do not match." data-val-equalto-other="*.Password" data-val-required="The Password2 field is required." id="Password2" name="Password2" value="" />
    <span class="field-validation-valid" data-valmsg-for="Password2" data-valmsg-replace="true"></span>
</div>
<div>
    <input type="submit" />
</div>

Validation is activated by the inclusion of the data-val attribute with a value of true which has been applied to the span elements targeted by the validation message tag helper. Various other data-val-* attributes are added as part of the tag helper rendering to specify the type of validation required and the error message, which can be customised as part of the attribute declaration:

[Compare(nameof(Password)), ErrorMessage ="Make sure both passwords are the same")]
public string Password2 { get; set; }
<input type="text" 
    data-val="true" 
    data-val-equalto="Make sure both passwords are the same" 
    data-val-equalto-other="*.Password" 
    data-val-required="The Password2 field is required." 
    id="Password2" name="Password2" value="" />

Validation message or summary tag helpers are required to provide somewhere for the error message to be displayed. Without these, any attempt to submit a form that fails client-side validation will not succeed, but without any visual clues as to why, potentially leaving the user confused.

Server side validation

Client side validation will not take place unless you include _ValidationScriptsPartial.cshtml in your form, or if you don't use tag helpers to generate the HTML for your form controls.

There are a number of other ways to circumvent client-side validation:

  • Use the browser's developer tools to change data-val="true" to data-val="false"
  • Save a copy of the form to your desktop and remove the validation scripts
  • Use Postman, Fiddler or Curl to post the form values directly
  • etc...

Because it is so easy to circumvent client-side validation, server-side validation is included as part of the ASP.NET Core validation framework. Once property values have been bound, the framework looks for all validation attributes on those properties and executes them. Any failures result in an entry being added to a ModelStateDictionary - a dictionary-like structure where validation errors are stored. This is made available in the PageModel class via ModelState, which has a property named IsValid that returns false if any of the validation tests fail:

public IActionResult  OnPost()
{
    if (ModelState.IsValid)
    {
        // do something
        return RedirectToPage("Contact"));
    }
    else
    {
        return Page();
    }
}

The snippet above illustrates the most common pattern for dealing with validation in an OnPost handler - query ModelState's IsValid property, and if it returns true, process the form otherwise redisplay the form, and let the framework take care of extracting error messages from ModelState and passing them to the validation message and/or summary tag helpers. If you use this approach, the values submitted by the user will be retained in the form for the user to modify accordingly.

If the form post passes validation, the PRG (Post-Redirect-Get) pattern is used to minimise the possibility of duplicate submission of form values.

Route Contraints

Input validation has so far focused on form submissions, but input can also be provided by the user in URLs as route values. In fact, URLs are the most common attack vector for those whose intent is malicious. Therefore, validating route parameter values for presence, data type and range is not to be overlooked.

Constraints provide a way to disambiguate routes but they also act as a means of specifying a white list of acceptable values. There is a wide range of constraints available, and they can be combined to form very restrictive rules that must be satisfied by the incoming value. If the constraints aren't satisfied, the framework returns a 404 Not Found instead of raising an exception.

You can read more about applying constraints in the routing topic.

Last updated: 10/5/2023 8:27:43 AM

© 2018 - 2024 - Mike Brind.
All rights reserved.
Contact me at Mike dot Brind at Outlook.com