It’s fairly common to use hidden form fields to store a value that gets posted back to your controller for the purposes of establishing context.
For example, on an edit form, you might render a hidden field containing the ID of the entity you’re editing. This is actually preferable to using route values because it’s much easier to tamper with those than it is to tamper with form data.
That said…it’s still pretty darn easy to tamper with form data. If you’re using FireBug, you can view the current page’s DOM and fiddle with any value you please. This can be catastrophically bad if, for example, an attacker views a form to edit an entity they have access to, and then changes the entity ID field to the ID of an entity they don’t have access to. Granted, your validation code should handle that case, but when it comes to attacks, an ounce of prevention is worth a pound of cure.
So, how to prevent an attacker from tampering with our hidden fields?
One solution is to create a secure hash of the field in question and render that to the client as well. Then, on postback, validate that the hash of the incoming value matches the original hash. Here’s an example of the HTML for that:
<input id="productId" name="productId" type="hidden" value="1" />
<input id="productId_sha1" name="productId_sha1" type="hidden" value="vMrgoclb/K+6EQ+6FK9K69V2vkQ=" />
The second hidden field is the SHA1 hash of the productId value 1, plus a secret value that makes tampering with the hash impossible.
I’ve written an HTML helper that facilitates creating these hash fields. Here’s how it’s used:
<% using(Html.BeginForm()) { %>
<%=Html.SecuredHiddenField("productId", 1) %>
<fieldset>
<p><label for="Name">Product Name:</label> <%=Html.TextBox("productName") %></p>
<button name="save" value="with">Save Product</button>
</fieldset>
<% } %>
And here’s the code for the HTML helper:
public static class SecuredValueHtmlHelper
{
public static string SecuredHiddenField(this HtmlHelper htmlHelper, string name, object value)
{
var html = new StringBuilder();
html.Append(htmlHelper.Hidden(name, value));
html.Append(GetHashFieldHtml(htmlHelper, name, GetValueAsString(value)));
return html.ToString();
}
public static string HashField(this HtmlHelper htmlHelper, string name, object value)
{
return GetHashFieldHtml(htmlHelper, name, GetValueAsString(value));
}
public static string MultipleFieldHashField(this HtmlHelper htmlHelper, string name, IEnumerable values)
{
var valueToHash = new StringBuilder();
foreach (var v in values)
{
valueToHash.Append(v);
}
return HashField(htmlHelper, name, valueToHash);
}
private static string GetValueAsString(object value)
{
return Convert.ToString(value, CultureInfo.CurrentCulture);
}
private static string GetHashFieldHtml(HtmlHelper htmlHelper, string name, string value)
{
return htmlHelper.Hidden(SecuredValueFieldNameComputer.GetSecuredValueFieldName(name),
SecuredValueHashComputer.GetHash(value));
}
}
The HTML helper uses a class called SecuredValueHashComputer to compute the SHA1 hash. I’ve written it so that you can plug in any hash computer that you want (MD5, or say SHA512 for the really paranoid):
public static class SecuredValueHashComputer
{
public static string Secret { get; set; }
public static IHashComputer HashComputer { get; set; }
static SecuredValueHashComputer()
{
Secret = "zomg!";
HashComputer = new SHA1HashComputer();
}
public static string GetHash(string value)
{
return HashComputer.GetBase64HashString(value, Secret);
}
}
In an ideal world, you’d load the value for “Secret” from your Web.config (preferably from an encrypted section) and use an IoC container to instantiate the IHashComputer.
Finally, validate the data like so:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(int productId, string productName, string save, FormCollection formValues)
{
if (save == "with")
{
SecuredValueValidator.ValidateValue(formValues, "productId");
}
ViewData["message"] = string.Format("Product ID {0} was saved!", productId);
return View();
}
The complete source code is available at http://blog.slatner.com/downloads/SecuredFormExample.zip.