TLDR; Sitecore Forms is vulnerable for overposting which enables end-users to disable field validations.
I’ve noticed that Sitecore Forms uses the default ASP.NET MVC model binding, and it binds the posted model to the FieldViewModel. As there is no validation on which properties of the FieldViewModel can be bound, we can post fields that should not be changeable by the end user, such as; if a field should be required, and even which content types for file uploads are allowed. I’ve created two scenarios to demonstrate the problem.
Scenario 1
We create a form with a Single Line Text, and a single Submit Button with the Submit Action “Save Data”, and we set the “Mandatory” checkbox on the Single Line Text.
This will create a form in the frontend where the Single Line Text field is required, and a normal submit post would look as follows:
As you can see the fxb.30da5a39-2ca9-487a-89c5-5b9f4328569f.Fields[2a03d066-f884-4106-8bf3-b5c18e79e287] field is my Single Line Text field. Now if I wanted to post the form without the required field data, then I’d just change the posted form data to my liking:
As you can see in the screenshot above I’ve removed the fxb.30da5a39-2ca9-487a-89c5-5b9f4328569f.Fields[2a03d066-f884-4106-8bf3-b5c18e79e287].Value from my post body, so the field will be empty. I’ve added the fxb.30da5a39-2ca9-487a-89c5-5b9f4328569f.Fields[2a03d066-f884-4106-8bf3-b5c18e79e287].Required to the post body, which states False. This now completely disables the Server Side validation for this field, and I can succesfully post the form.
Scenario 2
The example in scenario 1 only changes the Required field, so we can bypass required fields, however, as the model binding applies to ALL public properties in the ViewModel, I’d like to demonstrate another scenario.
Create a form with a File Upload field and a Submit Button with the action Save Data.
Set the “File Limit” to 2, set the “Limit File Types” to “text/plain”, and enable the checkbox for “File Count Validator” and “File Type Validator”.
A normal post from the form could be as follows (an empty TXT file is uploaded):
If I wanted to post two JPG files instead of TXT files, I’d just change the post body as follows:
As you can see I’m posting the fxb.30da5a39-2ca9-487a-89c5-5b9f4328569f.Fields[665d0d83-7c0a-41f2-a862-fa8723074e99].MaxFileCount to change the amount of allowed files, and the fxb.30da5a39-2ca9-487a-89c5-5b9f4328569f.Fields[665d0d83-7c0a-41f2-a862-fa8723074e99].AllowedContentTypes to change the allowed content types. In my example I’ve posted two JPEG files, which are accepted by the server.
The solution
There are multiple ways to solve this, however there’s one easy solution where you do not have to re-create all FieldViewModels that Sitecore uses for Sitecore Forms.
SecureFieldModelBinder.cs
This class only allows properties to be bound that are defined within the _allowedFields variable. Everything that is not included is not bound to the posted data.
namespace Example { public class SecureFieldModelBinder : FieldModelBinder { private static List<string> _allowedFields = new List<string>() {"Files", "ItemId", "Value", "ConfirmEmail", "ConfirmPassword"}; protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { if (_allowedFields.Contains(propertyDescriptor.Name)) base.BindProperty(controllerContext, bindingContext, propertyDescriptor); } } }
SecureFieldModelBinderDictionary.cs
This class makes sure that we can replace the hardcoded reference to the FieldModelBinder within the FormDataModelBinder within Sitecore Forms.
using System; using System.Linq; using System.Web.Mvc; using Sitecore.ExperienceForms.Models; namespace Example { public class SecureFieldModelBinderDictionary : ModelBinderDictionary { public override IModelBinder GetBinder(Type modelType, bool fallbackToDefault) { var modelBinder = System.Web.Mvc.ModelBinders.Binders.GetBinder(modelType, fallbackToDefault); return modelBinder is DefaultModelBinder && modelType.GetInterfaces().Contains(typeof(IViewModel)) ? new SecureFieldModelBinder() : base.GetBinder(modelType, fallbackToDefault); } } }
SetCustomModelBinders.cs
This class registers our new Model Binder to be used for all objects of the type FieldViewModel.
using Sitecore.ExperienceForms.Models; using Sitecore.ExperienceForms.Mvc; using Sitecore.ExperienceForms.Mvc.Models.Fields; namespace Example { public class SetCustomModelBinders { public void Process(PipelineArgs args) { System.Web.Mvc.ModelBinders.Binders[typeof(FieldViewModel)] = new SecureFieldModelBinder(); System.Web.Mvc.ModelBinders.Binders[typeof(FormDataModel)] = new FormDataModelBinder(_formRenderingContext, new SecureFieldModelBinderDictionary()); } } }
Example.Feature.Forms.SecureModelBinder.config
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <initialize> <processor type="Example.SetCustomModelBinders, Example" patch:after="processor[@type='Sitecore.ExperienceForms.Mvc.Pipelines.Initialize.SetModelBinders, Sitecore.ExperienceForms.Mvc']" /> </initialize> </pipelines> </sitecore> </configuration>
Remarks
Conditional required fields
Please note that the solution as stated above is a quick fix, and breaks required fields that are hidden by using conditions in your form. When Sitecore hides a field for the frontend, it posts the Required=False for that field, to disable it’s validation. If you want to keep using this method and take the risk that other fields that are required can be bypassed, add Required to the _allowedFields in the SecureFieldModelBinder.
Sitecore’s response:
A fix will be considered for future releases by the Product Management Team. If you’d like to open a support issue for this, you can use public reference number #490008.
Sitecore versions:
I’ve tested the existence of this issue on the following versions:
- Sitecore 10.1.0
- Sitecore 9.2
- Sitecore 9.1