I have been recently researching options to sanitize or filter the HTTP response before returning to the client. The challenge is an enterprise web system with a blend of legacy designs, frameworks, technologies and coding standards. My assignment is stand-up a training instance, which delivers filtered content and modifies the style. The effort required to analyze and scrub the legacy web components would be significant, so this path will be time consuming and costly.

The first option introduces a process injected into the ASP.NET HTTP pipeline to intercept the response before it is returned to the client. This can be accomplished using a custom HttpModule leveraging the HttpResponse.Filter property. The HttpModule exposes the HttpResponse using a custom HttpHandler. The HttpResponse.Filter is a wrapper to transform the HTTP body before passing along to the client. So let’s take a look at a simple implementation to perform a text-based search and replace applied to the HttpResponse.

Since we would like to customize the search/replace requests, we will create a custom web configuration section. This will allow us to add search/replace requests to the web.config or machine.config instead of hard-coding. The following are the configuration classes, which define the elements and attributes of the custom section. The SanitizeElement class contains the name, search and replace properties, which will represent the XML attributes for our add element. Since we would like to accept multiple search/replace requests, the SanitizeElementCollection class exposes the SanitizeElement as a collection. Finally, the SanitizeResponseSection class defines the SanitizeElementCollection as the transforms XML element.

01
    /// <summary>
02
    /// Configuration Section Add Element attributes
03
    /// </summary>
04
    public class SanitizeElement : ConfigurationElement  
05
    {
06
 
07
        [ConfigurationProperty("name", IsKey = true, IsRequired = true)]  
08
        public string Name 
09
        {
10
            get { return (string)this["name"]; }
11
            set { this["name"] = value; }
12
        }  
13
 
14
        [ConfigurationProperty("search", IsRequired = true)]  
15
        public string Search 
16
        {
17
            get { return (string)this["search"]; }
18
            set { this["search"] = value; }
19
        }  
20
 
21
        [ConfigurationProperty("replace", IsRequired = true)]  
22
        public string Replace 
23
        {
24
            get { return (string)this["replace"]; }
25
            set { this["replace"] = value; }
26
        }  
27
    }

01
    /// <summary>
02
    /// Configuration Element Collection
03
    /// </summary>
04
    [ConfigurationCollection(typeof(SanitizeElement))]
05
    public class SanitizeElementCollection : ConfigurationElementCollection  
06
    {
07
        protected override ConfigurationElement CreateNewElement()
08
        {
09
            return new SanitizeElement();
10
        }
11
 
12
        protected override object GetElementKey(ConfigurationElement element)
13
        {
14
            return ((SanitizeElement)element).Name;
15
        }
16
    }

01
    /// <summary>
02
    /// Custom Configuration Section for the SanitizeResponseModule, so 
03
    /// you can define a collection of transforms for the search
04
    /// and replace. 
05
   /// </summary>
06
    public class SanitizeResponseSection : ConfigurationSection
07
    {
08
        [ConfigurationProperty("transforms", IsDefaultCollection = true)]
09
        public SanitizeElementCollection Transforms 
10
        {
11
            get { return (SanitizeElementCollection)this["transforms"]; }
12
            set { this["transforms"] = value; }
13
        }
14
    }

You can define the custom section in the web.config using the following snippet. The custom section also appears below, which contains one or more transforms defining the search/replace criteria.

01
<?xml version="1.0"?>
02
<configuration>
03
  <configSections>
04
    <section name="sanitizeResponseModule"
05
             type="Joemwalton.Web.Configuration.SanitizeResponseSection,
06
                   Joemwalton.Web"/>
07
  </configSections>
08
  <sanitizeResponseModule>
09
    <transforms>
10
      <add name="URL" 
11
           search="www.asp.net"
12
           replace="joemwalton.com"/>
13
      <add name="Title"
14
           search="ASP.NET"
15
           replace="joemwalton.com"/>
16
      <add name="Content"
17
           search="Put content here"
18
           replace="This is my content"/>
19
    </transforms>
20
  </sanitizeResponseModule>
21
</configuration>

The custom SanitizeResponseModule HttpModule will perform the heavy-lifting to transform the HttpResponse applying the transforms in the custom web.config section. The HttpResponse.Filter requires a custom Stream to handle the transformation, so the following is the SanitizeStream class. The constructor accepts the standard input stream and transforms collection, which is retrieved from the custom web configuration section.

01
    /// <summary>
02
    /// SanitizeResponseModule
03
    /// HttpModule implementation to transform the HttpResponse stream using
04
    /// a simple text-basedsearch and replace. This can be used to sanitize
05
    /// the response and replace hardcoded values. You can create
06
    /// additional custom HttpModules to transform the HttpResponse including 
07
    ///  adding footers, replacing scripts, change the style sheet and almost anything.
08
    /// </summary>
09
    public class SanitizeResponseModule : IHttpModule
10
    {
11
        //Retrieve the transforms defined in our web configuration at the
12
        //custom sanitizeResponseModule section
13
        private SanitizeResponseSection config = ConfigurationManager.GetSection("sanitizeResponseModule") as SanitizeResponseSection;
14
 
15
        public SanitizeResponseModule() { }
16
 
17
        public void Init(HttpApplication obj)
18
        {
19
            //Add ReleaseRequestState event using SanitizeResponse
20
            obj.ReleaseRequestState += new EventHandler(this.SanitizeResponse); 
21
        }
22
 
23
        public void Dispose() { }
24
 
25
        private void SanitizeResponse(object objSender, EventArgs args)
26
        {
27
            HttpResponse response = HttpContext.Current.Response;
28
            if (response.ContentType == "text/html" &&
29
                config != null)
30
                //Use Response.Filter to transform the current response
31
                //using the SanitizeStreambefore returing to client
32
                response.Filter = new SanitizeStream(response.Filter,
33
                                                     config.Transforms);
34
        }
35
    }

001
    /// <summary>
002
    /// A very simple example of a transform Stream implementation to perform
003
    ///  a text based search and replace operation. The override Write()
004
    ///  method is handling all the transformation of the HttpResponse
005
    ///  stream. The SanitizeElementCollection defines the search and replace
006
    /// request, which will be set and retrieved from the web configuration
007
    ///  (web or machine.config).
008
    /// NOTE: try the MemoryStream efficiency and always consider the
009
    ///  performance impact of transforming the HttpResponse.
010
    /// </summary>
011
    public class SanitizeStream : Stream
012
    {
013
        private long _position;
014
        private Stream _stream;
015
        private SanitizeElementCollection _searchReplace;
016
 
017
        /// <summary>
018
        /// Base Constructor
019
        /// </summary>
020
        /// <param name="stream"></param>
021
        public SanitizeStream(Stream stream)
022
            : this(stream, new SanitizeElementCollection())
023
        { }
024
 
025
        /// <summary>
026
        /// Constructor with SanitizeElementCollection parameter
027
        /// </summary>
028
        /// <param name="stream"></param>
029
        /// <param name="searchReplace"></param>
030
        public SanitizeStream(Stream stream,
031
                              SanitizeElementCollection searchReplace)
032
        {
033
            _stream = stream;
034
            _searchReplace = (searchReplace != null ? searchReplace : new SanitizeElementCollection());
035
        }
036
 
037
        public override bool CanRead
038
        {
039
            get { return true; }
040
        }
041
 
042
        public override bool CanSeek
043
        {
044
            get { return true; }
045
        }
046
 
047
        public override bool CanWrite
048
        {
049
            get { return true; }
050
        }
051
 
052
        public override void Close()
053
        {
054
            _stream.Close();
055
        }
056
 
057
        public override void Flush()
058
        {
059
            _stream.Flush();
060
        }
061
 
062
        public override long Length
063
        {
064
            get { return 0; }
065
        }
066
 
067
        public override long Seek(long offset, SeekOrigin origin)
068
        {
069
            return _stream.Seek(offset, origin);
070
        }
071
 
072
        public override void SetLength(long length)
073
        {
074
            _stream.SetLength(length);
075
        }
076
 
077
        public override long Position
078
        {
079
            get { return _position; }
080
            set { _position = value; }
081
        }
082
 
083
        public override int Read(byte[] buffer, int offset, int count)
084
        {
085
            return _stream.Read(buffer, offset, count);
086
        }
087
 
088
        /// <summary>
089
        /// Override Stream Write method to perform the search
090
        ///  and replace operation.
091
        /// </summary>
092
        /// <param name="buffer"></param>
093
        /// <param name="offset"></param>
094
        /// <param name="count"></param>
095
        public override void Write(byte[] buffer, int offset, int count)
096
        {
097
            string transformed = UTF8Encoding.UTF8.GetString(buffer);
098
            foreach (SanitizeElement sanitize in _searchReplace)
099
            {
100
                if (!string.IsNullOrEmpty(sanitize.Search))
101
                {
102
                    transformed = transformed.Replace(sanitize.Search,
103
                        (string.IsNullOrEmpty(sanitize.Replace) ? string.Empty : sanitize.Replace));
104
                }
105
            }
106
            byte[] data = UTF8Encoding.UTF8.GetBytes(transformed);
107
            _stream.Write(data, offset, data.Length);
108
        }
109
    }

The following is the rendered page of the standard ASP.NET web application before we apply the filter using the custom SanitizeResponseModule.

Response Sanitizer Page Before Filter

Response Sanitizer Page Before Filter

The following is the web configuration entry to register the custom SanitizeResponseModule.

1
    <httpModules>
2
      <add name="SanatizeResponse"
3
           type="Joemwalton.Web.Http.Modules.SanitizeResponseModule,
4
                 Joemwalton.Web"/>
5
    </httpModules>

After we register the HttpModule in the web.config, the following is the filtered page. The search and replace transforms were applied, so the page reflects the changes.

Response Sanitizer Page After Filter

Response Sanitizer Page After Filter

So…this seems like a simple technique to apply a filter. What are some of the drawbacks? First, the additional load on the ASP.NET HTTP pipeline will impact the performance. In my case, this is not a production site and the performance hit is an acceptable alternative. Second, the HttpResponse Flush and End calls will possibly bypass the custom HttpModule. Finally, the handling of the Stream Write() calls managing a larger response could result in multiple calls and partial filters. I plan to thoroughly test the known issues with this prototype before proceeding with a solution, but this is a promising start.

I hope you find this post helpful and a possible solution for HttpRequest or HttpResponse filtering. You will find a variety of beneficial implementations including authorization/authentication, response compression, removing white-space, collecting/logging information and applying a dynamic footer. If we had a well-designed enterprise system then implementing a filter would probably not be necessary. We could also follow the configuration technique (see post) to tokenize environment information during the build and deploy, but this would be difficult to achieve with many legacy systems. We are sometimes forced to choose a solution due to the limitations of the current system.