Good Good Not Great

Some sort of a decision procedure

Replace all occurrences of something in the HTTP response

17 Sep 2018 | Estimated read time about 6 minutes

Categories: (Episerver) (MVC) (How to)

A while ago one of our clients had their site running on Episerver CMS penetration tested and naturally there were no real concerns, so instead of getting back a "Nothing to worry about" email the security company decided to pad their report with a list of potential issues that could be addressed. One of the issues was that all links opening a new window aka target="_blank" should actually include the rel attribute as well to end up as target="blank" rel="noopener noreferrer".

Unfortunately, the site had been running in production for quite some time and nobody felt like manually editing hundreds of published pages to update links that open in a new window, plus that would leave room for possibly unaware future editors to undermine all of the hard work.

With that option off of the table the next idea was to override the internal rendering of links and add the rel attribute manually, but soon I realized that there are multiple places in code that would also need to be updated and it sounded like a lot of work (deadlines and all).

The client suggested we create a script to run against the database and update all content at rest, that definitely wasn't the route we would be going.

I decided an action filter on the Base Page controller would be as good an implementation as any and promptly got it running exactly as needed by overriding the OnActionExecuting and OnResultExecuted functions.

The SecureTargetBlankLinksFilter.cs looked something like this

using System.IO;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.UI;

namespace GoodGoodNotGreat.Sample.Business.Filters
{
    public class SecureTargetBlankLinksFilter : ActionFilterAttribute
    {
        //get the response while it is executing and write it to a StringWriter
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var stringWriter = new StringWriter(new StringBuilder());
            var textWriter = new HtmlTextWriter(stringWriter);

            filterContext.HttpContext.Items["__output_executing__"] = (HttpWriter)filterContext.RequestContext.HttpContext.Response.Output;
            filterContext.RequestContext.HttpContext.Response.Output = textWriter;
            filterContext.HttpContext.Items["__string_writer_executing__"] = stringWriter;
        }

        //read the String Writer contents and perform replace
        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            var response = filterContext.HttpContext.Items["__string_writer_executing__"].ToString();
            
            if (!string.IsNullOrWhiteSpace(response))
            {
                response = response.Replace("target=\"_blank\"", "target=\"_blank\" rel=\"noopener noreferrer\"");
            }            

            var output = (HttpWriter) filterContext.HttpContext.Items["__output_executing__"];
            output.Write(response);
        }
    }
}

The filter worked great and deadlines were made well within everyone's capacity for chaos. Happiness.

A couple of months later the client had more Episerver CMS training and decided they would like to start using A/B Testing on their site, except that it didn't seem to work and just showed an empty white page. Chopping it up to not being implemented fully they asked me to do so.

I immediately suspected the ActionFilter I built would be to blame, writing the response out OnResultExecuted just felt too soon as I knew the Episerver A/B Testing Module would also need a chance to hook into the response pipeline and do its magic. Turns out my gut was right, disabling the Attribute filter gave the A/B Test a fair chance. Right, symptom fixed; but we still need to secure all target blank links sitewide. Time to put on a thinking cap.

Wait a minute, why don't we just hook up to an ASP.NET event instead of the MVC events which are causing us trouble. Turns out this would be exactly what we need.

We need to create our SecureTargetBlankLinksHttpModule.cs to hook up to and process the response

using System;
using System.IO;
using System.Text;
using System.Web;

namespace GoodGoodNotGreat.Sample.Business.Modules
{
	public class SecureTargetBlankLinksHttpModule : IHttpModule
	{
		// In the Init method, register HttpApplication events by adding event handlers.
		public void Init(HttpApplication httpApplication)
		{
			httpApplication.ReleaseRequestState += HttpApplication_OnReleaseRequestState;
		}
		
		private void HttpApplication_OnReleaseRequestState(object sender, EventArgs e)
		{
			//get the response
			var httpResponse = HttpContext.Current.Response;
			if (httpResponse.ContentType == "text/html")
			{
				//add a filter
				httpResponse.Filter = new SecureTargetBlankLinksResponseStream(httpResponse.Filter);
			}
		}

		//I feel like maybe we should do something here, please let me know
		public void Dispose() {}
	}

	public class SecureTargetBlankLinksResponseStream : MemoryStream
	{
		private readonly Stream _responseStream;

		public SecureTargetBlankLinksResponseStream(Stream inputStream)
		{
			_responseStream = inputStream;
		}

		public override void Close()
		{
			//get the response and apply the replace
			var response = Encoding.UTF8.GetString(ToArray());
			response = response.Replace("target=\"_blank\"", "target=\"_blank\" rel=\"noopener noreferrer\"");

			var bytes = Encoding.UTF8.GetBytes(response);
			_responseStream.Write(bytes, 0, bytes.Length);
			_responseStream.Flush();

			base.Close();
		}
	}
}

and then we just need to add the HttpModule to our Web.Config

<system.webServer>
    <modules>
      <add name="SecureTargetBlankLinksHttpModule" type="GoodGoodNotGreat.Sample.Business.Modules.SecureTargetBlankLinksHttpModule" />
    </modules>
</system.webServer>

And there you have it, all a targets now have their rel specified as well. Massive internet high-five to "rene" for the SO answer here.

Thanks for stopping by.

Recommended Posts