After extensive discussion among the team, we've decided to make a last-minute change to ASP.NET MVC 2 in regards to way that validation is handled. The conversation was kick-started by my blog post about the Required attribute and what it does (and does not) mean. More importantly, I want to re-address the security issues I brought up in the last post, now in the context of Model Validation, to understand whether this change makes your applications more secure.
To understand what we did, let's start with where we were a couple days ago.
Input Validation
Let's start out with a sample model:
public class Contact {
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
And a view:
First name: <%= Html.TextBox("FirstName") %><br>
Last name: <%= Html.TextBox("LastName") %><br>
Age: <%= Html.TextBox("Age") %>
Finally, here is our action method:
[HttpPost]
public ActionResult Edit(int id, Contact contact) {
if (ModelState.IsValid) {
// Submit values to the database
return RedirectToAction("Index");
}
return View("Edit");
}
When your Contact object is being model-bound, we will automatically run validation. This was true in ASP.NET MVC 1.0 as well, though we didn't have a pluggable validation system so most people weren't even aware that we had any validation support. The DefaultModelBinder class had validation hooks as well as built-in support for IDataErrorInfo for validation.
In addition to validation, there are also possible model-binding errors. The two major categories of these are: (1) data which is not compatible with the destination type (f.e., submitting "dog" for an integer), and (2) not submitting any data for a value which always needs data (f.e., a non-nullable value type like integer).
These kinds of errors are handled by the model-binding system before validation even happens. In MVC 1.0, these two situations were represented by the messages "The value 'dog' is not valid." and "A value is required."
In the early MVC 2 previews, we added a pluggable validation system, and in-box we provide support for the validation attributes in System.ComponentModel.DataAnnotations (as well as backward compatibility with IDataErrorInfo, though we suggest people migrate away from that at their convenience).
The implementation in those early previews is what we would call "Input Validation". The prevailing feature of Input Validation is that we would only run the validators for properties for which there were form inputs.
Let's pretend all three of the properties of our Contact model (besides ID) now have [Required] attributes on them. With the form in the view above, if you failed to provide a value for any of those properties, you could get a validation error telling you that the field was required. Checking ModelState.IsValid is how we know if there were any model binding or validation errors.
What happens in the case where we forget to provide a text box for LastName? The model binding system will create Contact objects for you, and fill in the FirstName and Age, while the LastName will remain null. There are no validation errors for LastName, because it wasn't part of the form.
This takes some people by surprise. In an Input Validation system, the purpose of [Required] is to ensure that if the user was offered an opportunity to provide a value for the field, we'll make sure it isn't empty. But what if the user wasn't offered the chance to edit that field? Does it make sense to enforce [Required]? More importantly, what if a bad guy decided to try to "under-post" your form by leaving off the LastName field?
In short: is validation a security system? Let's look at the impact of the change we made.
Model Validation
Earlier today, we committed a change to MVC 2 that converted the validation system from Input Validation to Model Validation.
What this means is that we will always run all validators on an object, if that object had at least one value bound into it during model binding. We run the property-level validators first, and if all of those succeed, we'll run the model-level validators. In the case of our Contact object with [Required] on the editable properties, if you accidentally left off the LastName property (or a bad guy "under-posted" the form without it), the validation system would see the null LastName value and trigger the error message.
What about complex objects inside of complex objects? Let's expand our Contact object to include address information:
public class Address {
[Required] public string Street { get; set; }
[Required] public string City { get; set; }
[Required] public string State { get; set; }
[Required] public string ZipCode { get; set; }
}
public class Contact {
public int ID { get; set; }
[Required] public string FirstName { get; set; }
[Required] public string LastName { get; set; }
[Required] public int Age { get; set; }
public Address HomeAddress { get; set; }
}
And let's use the same view:
First name: <%= Html.TextBox("FirstName") %><br>
Last name: <%= Html.TextBox("LastName") %><br>
Age: <%= Html.TextBox("Age") %>
Now, when we model bind the object, we will run the following validators:
- Contact.ID
- Contact.FirstName
- Contact.LastName
- Contact.Age
- Contact.HomeAddress (property-level and type-level, but not sub-properties)
- Contact (type-level)
When we say we're running the "Contact.HomeAddress" validators there, what I mean is that we're running any validators that are on the property itself, or on the Address type as model-level validators. That does not include Street, City, State, or ZipCode property validators. The reason we don't "dive" into the Address object recursively is that there was nothing in the form that bound any values inside of Address.
If the HomeAddress property had a [Required] attribute on it, it would fail, because HomeAddress would be null. It's null because we didn't bind any values into HomeAddress, and therefore never manufactured a new HomeAddress object.
Let's expand our view to include just the street of the address:
First name: <%= Html.TextBox("FirstName") %><br>
Last name: <%= Html.TextBox("LastName") %><br>
Age: <%= Html.TextBox("Age") %><br>
Street: <%= Html.TextBox("HomeAddress.Street") %>
Now when we validate, we'll run validators like this:
- Contact.HomeAddress.Street
- Contact.HomeAddress.City
- Contact.HomeAddress.State
- Contact.HomeAddress.Zip
- Contact.HomeAddress (type-level)
- Contact.ID
- Contact.FirstName
- Contact.LastName
- Contact.Age
- Contact.HomeAddress (property-level and type-level, but not sub-properties)
- Contact (type-level)
Now we'll get errors about missing values for City, State, and Zip, because this time we've bound at least one value inside of HomeAddress (namely, Street).
Given this new behavior, can we consider the [Required] attribute a security feature now? The answer is, sadly, still no. Even with Model Validation instead of Input Validation, we still have the same potentially unaddressed security issues that we've always had: "under-posting" and "over-posting". Switching to Model Validation might make it easier to address "under-posting" in certain scenarios, but we're by no means completely safe yet.
The "Under-Posting" Problem
The first problem is that [Required] on non-nullable properties doesn't quite behave like you'd expect. Literally, the implementation of the [Required] attribute is that the value cannot be null. Since a non-nullable value type can never be null, the [Required] attribute doesn't actually ever say the field is invalid.
So, what's it for, then? Failing to provide a value for a non-nullable property is actually a model binding error, as I mentioned above. We query the validation system and ask it for whatever it considers to be the "required" validator for the property, so we can determine what the message should be. The "required" validator is treated specially during model binding failures on non-nullable types, so that you're not stuck with the vanilla message 'A value is required.'
The off-shoot of this is that [Required] on a non-nullable value type cannot act as a guarantee that the form included a value. If it doesn't include a value, then model binding is skipped, which means the model binding failure won't occur. Additionally, when the [Required] validator is run, it queries the value from the model -- which will contain the value-types default value, typically 0 -- and say "that's not null, everything is all good here!".
If you make the property a nullable version of the value type, say by turning "int" into "int?", you'll be able to use [Required] as a way to ensure that a value is posted.
Another anomaly with [Required] happens when you use an ORM system like LINQ to SQL or Entity Framework. Rather than using action parameter binding, you might write an edit method like this:
[HttpPost]
public ActionResult Edit(int id) {
Contact contact = dataContact.Contacts.Single(c => c.ID == id);
if (TryUpdateModel(contact)) {
// Submit values to the database
return RedirectToAction("Index");
}
return View("Edit");
}
Now imagine that you've forgotten to provide the LastName editor, and you submit the form to the server. We happily go off to the server and retrieve the Contact object, and then attempt to update it with values from the form data. When we run the model validation system, the previous value for LastName will be intact, since there was no LastName in the form. It is presumably valid (it's in your database, after all), and so your data will go on its way, back to the database, without the LastName update.
Is "under-posting" a serious problem? It allows a bad-guy to bypass your input validation in some cases, which may cause you to pass on unexpected or invalid data upstream. Ideally, it will be caught at another layer, but only you know whether that kind of tampering could be ultimately destructive to your data. If you need to protect against it, you'll need to take additional steps to protect against it, which may include making your non-nullable values nullable and binding only against strict view-specific models.
The "Over-Posting" Problem
The other problem you could encounter is the "over-posting" problem. Let's say, for example, you have a blog and your comments are represented by this object from your ORM system:
public class Comment {
public int ID { get; set; } // Primary key
public int BlogID { get; set; } // Foreign key
public Blog Blog { get; set; } // Foreign entity
public string Name { get; set; }
public string Body { get; set; }
public bool Approved { get; set; }
}
And this is the view for the comments to be entered by users:
Name: <%= Html.TextBox("Name") %><br>
Comment: <%= Html.TextBox("Body") %>
The intention for comments is that they won't be approved by default. The blog owner will come along later and use the UI to approve all the non-spam comments.
What happens if the bad guy includes "Approved=true" in the form post? The model binder will happily set the Approved property to true. That's definitely not what you intended! It actually gets worse: if the bad guy guesses your entity object is called Blog, he might try posting values into fields like "Blog.Body" and actually be able to overwrite the body of the blog post. This is a potential disaster!
How can you prevent this? You have a couple choices here. First, you could use the [Bind] attribute, either on the Comment type or on the Comment action parameter, to indicate which properties are approved or disapproved for binding. Personally, I prefer the "white-list" approach so that only the named properties can be bound to, rather than the "black-list" approach (which excludes specific properties, and allows binding to everything else).
The other alternative is that the (Try)UpdateModel methods on Controller have overloads which accept white-/black-list parameter lists to tell the system which properties are eligible for binding.
There is a breaking change here from MVC 1.0 and the [Bind] attribute. Previously, we only validated the things we bound values to, so excluding a property with [Bind] also ensured that the validators for that property wouldn't run. In MVC 2 RTM, since we've changed to Model Validation, those validators will be run now, even though it's not possible to bind any values into the model. While the system still allows partial editing, you can no longer get partial validation like you could in 1.0 (or in the early MVC 2 previews).
Wrapping It All Up
I considered titling this blog post "[Required] Still Doesn't Do What You Think". :)
I hope it's still clear that, while the change from Input Validation to Model Validation might make some things more predictable, it still has not (and cannot) transform the validation system into a security feature. You still need to be careful to consider the implications of bad guys "under posting" and "over posting" to your forms, to ensure the safety and integrity of your data.
Recent Comments