Secure Sitecore : Cross Site Scripting (XSS) Vulnerability Prevention

In the last Cross Site Scripting (XSS) post: Secure Sitecore : Cross Site Scripting (XSS) Vulnerability Findings , we looked at how these attacks might look based on the browser the user is using. The interesting factor is that a potential attacker might not use a browser at all. You do not need a browser to initiate requests and process them. Fuzzing is a common technique where an attacker can create potentially invalid requests from scratch to perform testing on applications, to see how they respond.

Environment testing was done on two Windows 10 machines with Sitecore 8.1 update 3. Browsers use were Chrome (), Firefox (47.0.1), Microsoft Edge (25.10586.0.0 and Internet Explorer 11 (11.494.10586.0).

I would also recommend that you go through the post: Sitecore Security #3: Prevent XSS using Content Security Policy by Bas Lijten. Content headers can enforce security on browsers. Remember, each browser acts differently.

Based on the last post, I want to show you how to add in custom validation on requests. Because we are on 4.5, we have the ability to specify a Custom Request Validator. To do this lets do an sample in a true ASP.NET MVC application.

I created a sample MVC application with no custom coding. I added the following class for a custom Request Validator:

    public class CustomRequestValidation : RequestValidator
    {
        protected override bool IsValidRequestString(HttpContext context, string value, RequestValidationSource requestValidationSource, string collectionKey, out int validationFailureIndex)
        {
            validationFailureIndex = 0;

            switch (requestValidationSource)
            {
                case RequestValidationSource.QueryString:
                    WriteLog(value, requestValidationSource, collectionKey);
                    return false;

                    break;
            }

            return base.IsValidRequestString(context, value, requestValidationSource, collectionKey, out validationFailureIndex);
        }
}

Here is are the web.config entries related to compilation and httpRuntime:



The output is what we expected. Any request with a QueryString will and should result in an error.
xss21

The default ASP.NET MVC app, hits the return false which causes the invalid value error and stops.

Now lets take the default Sitecore install and make the same change by adding the CustomRequestValidation.



namespace TestXSS.Code
{
    public class CustomRequestValidation : RequestValidator
    {
        protected override bool IsValidRequestString(HttpContext context, string value, RequestValidationSource requestValidationSource, string collectionKey, out int validationFailureIndex)
        {
            validationFailureIndex = 0;

            switch (requestValidationSource)
            {
                case RequestValidationSource.QueryString:
                    return false;

                    break;
            }

            return base.IsValidRequestString(context, value, requestValidationSource, collectionKey, out validationFailureIndex);
        }
  }

Lets open the site using some QueryString params and see what happens.
xss23

As you can see, Sitecore somehow seems to move past it and continues to evaluate the other parameters. My initial thought was to some how detect the additional tags via the CustomRequestValidation but now that doesn’t look feasible.

There is processor called SupressFormValidation which triggers even before the CustomRequestValidation and here Sitecore basically suppresses the validations for specific urls. In doing so, my previous article show that we are getting inconsistent results. The default SupressFormValidation code is shown below:

using Sitecore.Diagnostics;
using Sitecore.Web;
using System;
using System.Collections.Specialized;
using System.Web;

namespace Sitecore.Pipelines.PreprocessRequest
{
    /// 
    /// The suppress form validation.
    /// 
    public class SuppressFormValidation : PreprocessRequestProcessor
    {
        public SuppressFormValidation()
        {
        }

        /// 
        /// Suppresses the form validation exception that has been introduced in .NET 4.0 for Sitecore backend.
        /// 
        /// The pipeline arguments.
        /// Indicates that form validation exception occured for frontend.
        public override void Process(PreprocessRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (WebUtil.ShouldIgnoreUrl(args.Context.Request.Url.AbsolutePath))
            {
                return;
            }
            try
            {
                NameValueCollection form = args.Context.Request.Form;
            }
            catch (HttpRequestValidationException httpRequestValidationException)
            {
                string rawUrl = args.Context.Request.RawUrl;
                if (!rawUrl.StartsWith("/sitecore/shell/", StringComparison.InvariantCultureIgnoreCase) && !rawUrl.StartsWith("/sitecore/admin/", StringComparison.InvariantCultureIgnoreCase) && !rawUrl.StartsWith("/-/speak/request/", StringComparison.InvariantCultureIgnoreCase))
                {
                    throw;
                }
            }
        }
    }
}

Here is a possible remedy. Get the web.config back to normal without the CustomRequestValidation. We validate our query string parameters against RFC 3986 and if its violated we throw an error. Here is the modified SupressFormValidation:

using Sitecore.Diagnostics;
using Sitecore.Pipelines.PreprocessRequest;
using System;
using System.Text.RegularExpressions;
using System.Web;

namespace TestXSS.Code
{
    public class SuppressFormValidation : Sitecore.Pipelines.PreprocessRequest.SuppressFormValidation
    {

        public override void Process(PreprocessRequestArgs args)
        {
            bool isShell = CheckSitecoreUrl(args.Context.Request.RawUrl);

            if (isShell)
            {
                base.Process(args);
            }
            else
            {
                try
                {
                    if (IsSecurePayload())
                        base.Process(args);
                }
                catch (HttpRequestValidationException)
                {
                    throw;
                }
            }
        }

        private bool CheckSitecoreUrl(string url)
        {
            string[] exceptions = new string[]
            {
                "/sitecore/",
                "/WebResource.axd",
                "/ScriptResource.axd",
                "/Telerik.Web.UI.WebResource.axd"
            };
            //"/sitecore/shell/",
            //"/sitecore/admin/",
            //"/sitecore/api/",

            foreach (string e in exceptions)
            {
                if (url.StartsWith(e, StringComparison.InvariantCultureIgnoreCase))
                {
                    return true;
                }
            }
            return false;
        }

        //just processing query string at this moment
        private bool IsSecurePayload()
        {
            bool returnVal = true;

            foreach (String key in HttpContext.Current.Request.QueryString.AllKeys)
            {
                string value = HttpContext.Current.Request.QueryString[key];
                if (!IsSecureParam(value))
                {
                    throw new HttpRequestValidationException("-*-*-*-*A potentially dangerous Request.RawUrl value was detected from the client " + value);
                }
            }

            return returnVal;
        }

        //RFC 3986 for query string - http://www.rfc-base.org/rfc-3986.html
        private bool IsSecureParam(string value)
        {
            if (!string.IsNullOrEmpty(value) && !Regex.IsMatch(value, @"^[a-zA-Z0-9@.' _~-]{1,40}$"))
                return false;
            else
                return true;
        }
    }
}

Here is the patch config:


  
    
        preprocessRequest>
            
               TestXSS.Code.SuppressFormValidation, TestXSS
             
      
    
  

Now if we run the tests using urls regardless of the encoding:

xss26

xss24

xss25

The system functions the way we expect it to on top of the base validation by ASP.NET. Now if we get in to the Sitecore admin, we have no issues modifying items etc.

xss27

This is not limited to what I did, you can evaluate Form posts, cookies etc. You can check encoding as well as any custom rules you have for your system to be secure. We no longer have security mishaps when Chrome or FF encode their inputs.

Remember, we did not set the validation mode to 2.0, we left the default web.config and sitecore.config which defaults to framework 4.5.

 
If you have any questions or concerns, please get in touch with me. (@akshaysura13 on twitter or on Slack).

6 thoughts on “Secure Sitecore : Cross Site Scripting (XSS) Vulnerability Prevention”

  1. How does a noncoder – at least not any more, figure out whether this is an issue? What kind of questions should I ask, Akshay?

    Best,
    Jey

  2. Hi Jey, thanks for reading my post and that was quick! 😉 This is something which should be in build and the in house team and the security team has to work hand in hand in preventing. In large organizations, it is the security team which takes this responsibility but I would hold the developers accountable as well. There are several tools available online which let you do penetration testing which will expose these flaws.

  3. Hi Akshay

    Why wouldn’t you simplify your code to the following?

    public override void Process(PreprocessRequestArgs args)
    {
    try
    {
    System.Collections.Specialized.NameValueCollection nameValues =
    HttpUtility.ParseQueryString(HttpContext.Current.Request.QueryString.ToString());
    base.Process(args);
    }
    catch (HttpRequestValidationException e)
    {
    throw;
    }
    }

  4. Since they are client based calls for Google tag manager and analytics, it should work. Any inbound links you can always exclude if you know the format.

Leave a Reply

Your email address will not be published. Required fields are marked *