Code Reference A collection of code for my reference (and perhaps other people too)

21Dec/090

Secure WCF REST webservice – part 1 – Validate XML with Schema

I have spent a good amount of time workig to implement a good XML validation method. I followed may leads and ended with a simple solution.

My goals were:
* use WCF/REST
* Use a Schema to validate the incoming XML request
* Extend the deserialize method turn prohibit DTDs and to disable external entities
* Filter IPs based on a list in the Config file
* Use the newer WebServiceHost2 from the WCF REST Starter Kit

Problems I had:
* I tried extending IDispatchMessageInspector and IClientMessageInspector to add schema validation. This worked great on WS Services (wsHttpBinding), but I could not get it to work on WebServices (webHttpBinding). I could not find any exampels of working solution on the net on how to do this with web services. (here is the blog post on what I did come up with). I abandoned this approach.

* Schema validation is done on the xml before it is derialized or when it is deserialized to an object. Because I could not access the deserialization before accepting the incoming xml request, I was forced to have the incoming object type 'XElement' rather than the destination object.

My solution:
* I used the new WCF REST Starter Kit. This reduces the confit file to almost nothing. The endpoints are set automatically. All adjustments can be done thru attributes on the service method itself.

Here is my config file:

<?xml version="1.0"?>
<configuration>
  <appSettings>
     ...
  </appSettings>

  <system.web>
		<compilation debug="true"/>
  </system.web>

  <system.serviceModel>
		<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
  </system.serviceModel>
</configuration>

My Service:

// The following line sets the default namespace for DataContract serialized typed to be ""
[assembly: ContractNamespace("", ClrNamespace = "ThisIsMyService")]

namespace ThisIsMyService
{
    // TODO: Please set IncludeExceptionDetailInFaults to false in production environments
    [ServiceBehavior(IncludeExceptionDetailInFaults = true), AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed), ServiceContract]
    public partial class Service
    {
        [WebHelp(Comment = "This is my service")]
        [WebInvoke(UriTemplate = "MyService")]
        [OperationContract]
        public XElement MyService(XElement requestXml)
        {
            // Schema validation and deserialization are both done at the same time.
            // If there is a problem, null is returned and an exception is thrown with a HTTPStatusCode of Bad Request.
            var validatedObject = Helper.XmlToObject(requestXml, typeof(RequestObject),
                ConfigurationManager.AppSettings["SchemaPath"].ToString(),
                ConfigurationManager.AppSettings["SchemaNamespace"].ToString());

            // I do all the process and other work in the helper class to keep this clean.
            if (validatedObject is RequestObject)
                return new Helper().DoStuff((RequestObject)validatedObject);

            // If there is problem validating the incoming request XML, then the validated object will be null
            // Error validating request - return nothing
            return new XElement("root");
        }
    }
}

My service is very basic. All the logic is done in the helper class.

My helper class contains the static validation XMLtoObject method and the methods for doing the work of the service.

When you deserialize XML to an object, you can add a schema that will be used to check the XML.
What you don't easily find in the documentation, is the fact that the Schema must be read and validated before it can be added to the reader that reads and deserializes the incoming xml request.

public class EngineServiceHelper
{
    /// <summary>
    /// A schema is an XML file like all the rest.
    /// It also must be loaded and deserialized.
    /// To use a schema, it must be placed in a XmlSchemaSet.
    /// </summary>
    /// <param name="xml"></param>
    /// <param name="DestinationType"></param>
    /// <param name="schemaPath"></param>
    /// <param name="xmlNamespace"></param>
    /// <returns></returns>
    public static object XmlToObject(XElement xml, Type DestinationType, string schemaPath, string xmlNamespace)
    {
        ErrorMessage = string.Empty;
        ErrorsCount = 0;

        XmlSchemaSet schemaSet = new XmlSchemaSet();
        schemaSet.ValidationEventHandler += new ValidationEventHandler(settings_ValidationEventHandler);
        schemaSet.Add(xmlNamespace, schemaPath);

        if (schemaSet.Count > 0)
        {
            try
            {
                // Set the setting for reading the Schema file
                XmlReaderSettings schemaSettings = new XmlReaderSettings();
                schemaSettings.ValidationType = ValidationType.Schema;
                schemaSettings.Schemas.Add(schemaSet);
                // To make the service more secure and prevent XML Bombs and such, disable DTDs
                xmlRequestSettings.ProhibitDtd = true;
                // Limit the size of the elements - 1024 = 1Kb
                schemaSettings.MaxCharactersFromEntities = 1024;
                // Adding the validation event handler prevents validation errors from throwing an exception.
                // We want to control when and how an exception is thrown.
                schemaSettings.ValidationEventHandler += new ValidationEventHandler(settings_ValidationEventHandler);
                // create the reader for reading the schema
                XmlReader schemaReader = XmlReader.Create(xml.CreateReader(), schemaSettings);

                // Set the settings for reading the incoming XML request
                XmlReaderSettings xmlRequestSettings = new XmlReaderSettings();
                // To make the service more secure and prevent XML Bombs and such, disable DTDs
                xmlRequestSettings.ProhibitDtd = true;
                // Limit the size of the elements - 1024 = 1Kb
                schemaSettings.MaxCharactersFromEntities = 1024;
                // Create the reader for reading the XML request.
                XmlReader xmlReader = XmlReader.Create(schemaReader, xmlRequestSettings);

                // Deserialize the XML to an object
                XmlSerializer serializer = new XmlSerializer(DestinationType, xmlNamespace);
                object returnObject = serializer.Deserialize(xmlReader);

                // If there are errors, throw an exception
                if (ErrorsCount > 0)
                    throw new Exception(ErrorMessage);

                // If ther are no problems, return the deserialized object
                return returnObject;
            }
            catch (Exception ex)
            {
                // Using the WebProtocolException in the WCF REST Starter Kit, you can throw
                // and exception and choose the HTTPStatusCode that will be displayed to the end user.
                throw new WebProtocolException(HttpStatusCode.BadRequest, "Your XML is invalid: " + ex.Message, null);
            }
        }
        return null;
    }

    /// <summary>
    /// Check the error severity and create the error message.
    /// Increment the error counter.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public static void settings_ValidationEventHandler(object sender, ValidationEventArgs e)
    {
        if (e.Severity == XmlSeverityType.Error)
        {
            ErrorMessage = "ERROR: " + ErrorMessage + e.Message + "\r\n";
            ErrorsCount++;
        }
        else
        {
            ErrorMessage = "WARNING: " + ErrorMessage + e.Message + "\r\n";
            ErrorsCount++;
        }
    }
}

The validation event handler allows you to catch the validation errors and handle them yourself.
I am checking the error severity and returning a message that reflects it.