ASP.NET Anti-forgery XFrame Options

Home / ASP.NET Anti-forgery XFrame Options

If you recall my previous post on ASP.NET Anti-forgery configuration options, you may be familiar with the way the ASP.NET MVC AntiForgeryToken helper adds the “x-frame-options SAMEORIGIN” header to server responses. This header prevents different domains from displaying your site in an iframe. Your only option to manage this feature is to completely disable it.

An all or nothing approach to configuration is quite inflexible. Additionally, if we are using the web.config to handle our configuration, that too is pretty rigid and hard to manage.


With ASP.NET, though, and especially MVC, you can really control how every response is created. You have OWIN that is a middleware handler that can do lots of helpful things, and you also have MVC action filters that you can attach to discrete controllers and controller actions, or apply globally.

In my case, I wanted to be able to display a single controller action in an iframe. The action in question allows the user to verify their password. Ideally, the controller action would be hosted on my SSO site to provide a consistent method for consuming applications to force a user to verify their password. This is similar to the way lots of sites ask a user to verify their identity even though they are already logged in. Since my SSO site is an OAuth endpoint, I already store a list of white listed sites. It made a lot of sense to take these same white list and allow those sites to open the “VerifyPassword” endpoint in an iframe.

While it has nothing to do with the proper response headers, if you’re curious, once the user verifies their password, JavaScript’s postMessage and a message event handler are used to let the parent window know that password verification has succeeded or failed. The iframe itself is hosted inside of an angular ui-modal for a nice user experience. The ui-modal’s promises are used to indicate success or failure.

Back to the header issue, another wrinkle in the issue is that not all browsers support “x-frame-options ALLOW-FROM.” Chrome doesn’t, Safari doesn’t, and maybe even Opera doesn’t. Technically, this header has been deprecated and replaced by “Content-Security-Policy frame-ancestors.” You can imagine that this would make managing this configuration in your web.config even more tedious.

Long story short, the filtering is pretty simple. I created an ActionFilterAttribute called “XFrameOptionsFilterAttribute.”

First, the filter looks to see if AntiForgeryConfig.SuppressXFrameOptionsHeader is set. If it’s not, or we explicitly tell the filter to always run, then we proceed to add or headers. Overridng the “OnResultExecuted” method ensures that we are running our header-adding-code after the action’s view is rendered. This is imported since it’s the Razor MVC helper that adds the SAMEORIGIN header.

The filter sniffs the browser and version to determine what browser the user is running. Based on a simple determination, we toggle between using X-Frame-Options or Content-Security-Policy. If the browser/version do not meet our requirements, they we default to adding the SAMEORIGIN header back into the response.

Applying the filter to a single action then becomes a matter of decorating the action:

[Authorize]
[XFrameOptionsFilterAttribute]
public virtual ActionResult VerifyPassword()
{
    var model = new VerifyPasswordModel();
    model.Username = User.Identity.Name;
    return View(model);
}

If you want to add the filter globally, you’d add it to your GlobalConfig, and more than likely, you’d want the filter always applied regardless of your AntiForgeryConfig (note AlwaysApply is set to true):

namespace MyServer
{
    public static class GlobalConfig
    {
        public static void CustomizeConfig(HttpConfiguration config)
        {
           // Register global XFrameOptions filter
            GlobalFilters.Filters.Add(new XFrameOptionsFilterAttribute() { AlwaysApply = true });

            // Set antiforgery config
            AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimsIdentity.DefaultNameClaimType;
            AntiForgeryConfig.RequireSsl = true;
        }
    }
}

As I mentioned, the acton filter is relatively straight-forward. One could also do the same thing with an OWIN interceptor, which actually may be more robust since action filters do not get applied in certain circumstances (like 302 redirect due to authorization). The full code for the action filter is below. Note that my “WhiteList” object has a string property with the URL.

PS – I used this site as a guide to determining which browsers/versions supposed CSP: http://caniuse.com/#feat=contentsecuritypolicy

using System;
using System.Linq;
using System.Web.Mvc;
using System.Collections.Generic;
using System.Web.Helpers;

namespace MyServer.Filters
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class XFrameOptionsFilterAttribute : ActionFilterAttribute
    {
        private string _xframeOptions = "X-Frame-Options";
        private string _contentSecurityPolicy = "Content-Security-Policy";

        public bool AlwaysApply { get; set; }

        public XFrameOptionsFilterAttribute() { }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            var response = filterContext.HttpContext.Response;

            // Assume that if we aren't suppressing the header, and it's not present, then we don't want the view suppressed.
            // This ties in better with the AntiForgery token expected behavior.  Otherwise, if the header is suppressed,
            // then we apply the filter to every response.
            if (!AntiForgeryConfig.SuppressXFrameOptionsHeader && !response.Headers.AllKeys.Any(x => x == _xframeOptions) && !AlwaysApply)
            {
                return;
            }

            // If the AntiForgeryConfig is set to add the SAMEORIGIN header, and we've made it this far, remove it.
            if (!AntiForgeryConfig.SuppressXFrameOptionsHeader)
            {
                response.Headers.Remove(_xframeOptions);
            }

            // If the refer is in the list, do nothing.  If they aren't, add SAMEORIGIN
            // This is assuming that browers all check the SAMEORIGIN policy on first loading and not subsequent refreshes.
            if (filterContext.HttpContext.Request.UrlReferrer != null &&
                (filterContext.HttpContext.Request.Url.Host != filterContext.HttpContext.Request.UrlReferrer.Host))
            {
                var requestUrl = filterContext.HttpContext.Request.UrlReferrer.GetLeftPart(UriPartial.Authority);
                if (!_whiteList.Any(x => x.Url.StartsWith(requestUrl)))
                {
                    response.AddHeader(_xframeOptions, "SAMEORIGIN");
                }
            }
        }

        private List<WhiteList> GetWhiteList(object[] param)
        {
            // Get your whitelist from where ever
            return whiteList;
        }
    }
}

Update: I simplified this attribute to only apply or remove the SAMEORIGIN header based on the referrer. This eliminates the need to guess at browser versions, whether CSP is supported, etc.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.