120

I have an ASP.NET 4.0 IIS7.5 site which I need secured using the X-Frame-Options header.

I also need to enable my site pages to be iframed from my same domain as well as from my facebook app.

Currently I have my site configured with a site headed of:

Response.Headers.Add("X-Frame-Options", "ALLOW-FROM SAMEDOMAIN, www.facebook.com/MyFBSite")

When I viewed my Facebook page with Chrome or Firefox my sites pages (being iframed with my facebook page) are display ok, but under IE9, I get the error:

"this page cannot be displayed…" (because of the X-Frame_Options restriction).

How do I set the X-Frame-Options: ALLOW-FROM to support more than a single domain?

X-FRAME-OPTION being a new feature seems fundamentally flawed if only a single domain can be defined.

Jim Aho
  • 9,932
  • 15
  • 56
  • 87
user1340663
  • 1,201
  • 2
  • 9
  • 3
  • 2
    This seems to be a known limitation: https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet#Limitations – Pierre Ernst Apr 18 '12 at 13:14

13 Answers13

132

X-Frame-Options is deprecated. From MDN:

This feature has been removed from the Web standards. Though some browsers may still support it, it is in the process of being dropped. Do not use it in old or new projects. Pages or Web apps using it may break at any time.

The modern alternative is the Content-Security-Policy header, which along many other policies can white-list what URLs are allowed to host your page in a frame, using the frame-ancestors directive.
frame-ancestors supports multiple domains and even wildcards, for example:

Content-Security-Policy: frame-ancestors 'self' example.com *.example.net ;

Unfortunately, for now, Internet Explorer does not fully support Content-Security-Policy.

UPDATE: MDN has removed their deprecation comment. Here's a similar comment from W3C's Content Security Policy Level

The frame-ancestors directive obsoletes the X-Frame-Options header. If a resource has both policies, the frame-ancestors policy SHOULD be enforced and the X-Frame-Options policy SHOULD be ignored.

katalin_2003
  • 787
  • 1
  • 16
  • 30
Kobi
  • 135,331
  • 41
  • 252
  • 292
  • 14
    frame-ancestors is marked as "experimental API and should not be used in production code" on MDN. + X-Frame-Options is not deprecated but "non-standard" but "is widely supported and can be used in conjunction with CSP" – Jonathan Muller Jun 08 '15 at 09:19
  • 1
    @JonathanMuller - The wording on `X-Frame-Options` changed, and is less severe now. It's a good point that it is risky to used a spec that isn't finalized. Thanks! – Kobi Jun 08 '15 at 10:26
  • 2
    I can't find the deprectated warning on MDN any more. Has Mozilla changed their opinion? – thomaskonrad Feb 03 '16 at 09:01
  • 2
    @to0om - Thanks! I updated the answer with another comment. I may have come on too strong in my answer. Either way, `X-Frame-Options` doesn't support multiple sources. – Kobi Feb 03 '16 at 09:57
  • 5
    @Kobi, I think the answer needs re-organizing. The very first sentence says that this is deprecated as per the MDN. It'll be less misleading if you add your update at the top (with a bold colored "UPDATE:"). Thanks. – Kasun Gajasinghe Aug 11 '16 at 05:34
  • 1
    Note that frame-ancestors will not take priority in firefox, see this bug https://bugzilla.mozilla.org/show_bug.cgi?id=1024557 and upvote it to try get this fixed. – MicWit Feb 21 '17 at 22:09
41

From RFC 7034:

Wildcards or lists to declare multiple domains in one ALLOW-FROM statement are not permitted

So,

How do I set the X-Frame-Options: ALLOW-FROM to support more than a single domain?

You can't. As a workaround you can use different URLs for different partners. For each URL you can use it's own X-Frame-Options value. For example:

partner   iframe URL       ALLOW-FROM
---------------------------------------
Facebook  fb.yoursite.com  facebook.com
VK.COM    vk.yoursite.com  vk.com

For yousite.com you can just use X-Frame-Options: deny.

BTW, for now Chrome (and all webkit-based browsers) does not support ALLOW-FROM statements at all.

Community
  • 1
  • 1
vbo
  • 13,583
  • 1
  • 25
  • 33
  • 1
    It looks like webkit now supports `ALLOW-FROM` using the link you provided. – Jimi Jun 30 '17 at 01:06
  • 3
    @Jimi No it doesn't - the last comment on the link in question, says you need to use a CSP policy instead. This option still doesn't work in Chrome. – NickG Dec 20 '17 at 13:17
11

Necromancing.
The provided answers are incomplete.

First, as already said, you cannot add multiple allow-from hosts, that's not supported.
Second, you need to dynamically extract that value from the HTTP referrer, which means that you can't add the value to Web.config, because it's not always the same value.

It will be necessary to do browser-detection to avoid adding allow-from when the browser is Chrome (it produces an error on the debug - console, which can quickly fill the console up, or make the application slow). That also means you need to modify the ASP.NET browser detection, as it wrongly identifies Edge as Chrome.

This can be done in ASP.NET by writing a HTTP-module which runs on every request, that appends a http-header for every response, depending on the request's referrer. For Chrome, it needs to add Content-Security-Policy.

// https://stackoverflow.com/questions/31870789/check-whether-browser-is-chrome-or-edge
public class BrowserInfo
{

    public System.Web.HttpBrowserCapabilities Browser { get; set; }
    public string Name { get; set; }
    public string Version { get; set; }
    public string Platform { get; set; }
    public bool IsMobileDevice { get; set; }
    public string MobileBrand { get; set; }
    public string MobileModel { get; set; }


    public BrowserInfo(System.Web.HttpRequest request)
    {
        if (request.Browser != null)
        {
            if (request.UserAgent.Contains("Edge")
                && request.Browser.Browser != "Edge")
            {
                this.Name = "Edge";
            }
            else
            {
                this.Name = request.Browser.Browser;
                this.Version = request.Browser.MajorVersion.ToString();
            }
            this.Browser = request.Browser;
            this.Platform = request.Browser.Platform;
            this.IsMobileDevice = request.Browser.IsMobileDevice;
            if (IsMobileDevice)
            {
                this.Name = request.Browser.Browser;
            }
        }
    }


}


void context_EndRequest(object sender, System.EventArgs e)
{
    if (System.Web.HttpContext.Current != null && System.Web.HttpContext.Current.Response != null)
    {
        System.Web.HttpResponse response = System.Web.HttpContext.Current.Response;

        try
        {
            // response.Headers["P3P"] = "CP=\\\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\\\"":
            // response.Headers.Set("P3P", "CP=\\\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\\\"");
            // response.AddHeader("P3P", "CP=\\\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\\\"");
            response.AppendHeader("P3P", "CP=\\\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\\\"");

            // response.AppendHeader("X-Frame-Options", "DENY");
            // response.AppendHeader("X-Frame-Options", "SAMEORIGIN");
            // response.AppendHeader("X-Frame-Options", "AllowAll");

            if (System.Web.HttpContext.Current.Request.UrlReferrer != null)
            {
                // "X-Frame-Options": "ALLOW-FROM " Not recognized in Chrome 
                string host = System.Web.HttpContext.Current.Request.UrlReferrer.Scheme + System.Uri.SchemeDelimiter
                            + System.Web.HttpContext.Current.Request.UrlReferrer.Authority
                ;

                string selfAuth = System.Web.HttpContext.Current.Request.Url.Authority;
                string refAuth = System.Web.HttpContext.Current.Request.UrlReferrer.Authority;

                // SQL.Log(System.Web.HttpContext.Current.Request.RawUrl, System.Web.HttpContext.Current.Request.UrlReferrer.OriginalString, refAuth);

                if (IsHostAllowed(refAuth))
                {
                    BrowserInfo bi = new BrowserInfo(System.Web.HttpContext.Current.Request);

                    // bi.Name = Firefox
                    // bi.Name = InternetExplorer
                    // bi.Name = Chrome

                    // Chrome wants entire path... 
                    if (!System.StringComparer.OrdinalIgnoreCase.Equals(bi.Name, "Chrome"))
                        response.AppendHeader("X-Frame-Options", "ALLOW-FROM " + host);    

                    // unsafe-eval: invalid JSON https://github.com/keen/keen-js/issues/394
                    // unsafe-inline: styles
                    // data: url(data:image/png:...)

                    // https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet
                    // https://www.ietf.org/rfc/rfc7034.txt
                    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
                    // https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

                    // https://stackoverflow.com/questions/10205192/x-frame-options-allow-from-multiple-domains
                    // https://content-security-policy.com/
                    // http://rehansaeed.com/content-security-policy-for-asp-net-mvc/

                    // This is for Chrome:
                    // response.AppendHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: *.msecnd.net vortex.data.microsoft.com " + selfAuth + " " + refAuth);


                    System.Collections.Generic.List<string> ls = new System.Collections.Generic.List<string>();
                    ls.Add("default-src");
                    ls.Add("'self'");
                    ls.Add("'unsafe-inline'");
                    ls.Add("'unsafe-eval'");
                    ls.Add("data:");

                    // http://az416426.vo.msecnd.net/scripts/a/ai.0.js

                    // ls.Add("*.msecnd.net");
                    // ls.Add("vortex.data.microsoft.com");

                    ls.Add(selfAuth);
                    ls.Add(refAuth);

                    string contentSecurityPolicy = string.Join(" ", ls.ToArray());
                    response.AppendHeader("Content-Security-Policy", contentSecurityPolicy);
                }
                else
                {
                    response.AppendHeader("X-Frame-Options", "SAMEORIGIN");
                }

            }
            else
                response.AppendHeader("X-Frame-Options", "SAMEORIGIN");
        }
        catch (System.Exception ex)
        {
            // WTF ? 
            System.Console.WriteLine(ex.Message); // Suppress warning
        }

    } // End if (System.Web.HttpContext.Current != null && System.Web.HttpContext.Current.Response != null)

} // End Using context_EndRequest


private static string[] s_allowedHosts = new string[] 
{
     "localhost:49533"
    ,"localhost:52257"
    ,"vmcompany1"
    ,"vmcompany2"
    ,"vmpostalservices"
    ,"example.com"
};


public static bool IsHostAllowed(string host)
{
    return Contains(s_allowedHosts, host);
} // End Function IsHostAllowed 


public static bool Contains(string[] allowed, string current)
{
    for (int i = 0; i < allowed.Length; ++i)
    {
        if (System.StringComparer.OrdinalIgnoreCase.Equals(allowed[i], current))
            return true;
    } // Next i 

    return false;
} // End Function Contains 

You need to register the context_EndRequest function in the HTTP-module Init function.

public class RequestLanguageChanger : System.Web.IHttpModule
{


    void System.Web.IHttpModule.Dispose()
    {
        // throw new NotImplementedException();
    }


    void System.Web.IHttpModule.Init(System.Web.HttpApplication context)
    {
        // https://stackoverflow.com/questions/441421/httpmodule-event-execution-order
        context.EndRequest += new System.EventHandler(context_EndRequest);
    }

    // context_EndRequest Code from above comes here


}

Next you need to add the module to your application. You can either do this programmatically in Global.asax by overriding the Init function of the HttpApplication, like this:

namespace ChangeRequestLanguage
{


    public class Global : System.Web.HttpApplication
    {

        System.Web.IHttpModule mod = new libRequestLanguageChanger.RequestLanguageChanger();

        public override void Init()
        {
            mod.Init(this);
            base.Init();
        }



        protected void Application_Start(object sender, System.EventArgs e)
        {

        }

        protected void Session_Start(object sender, System.EventArgs e)
        {

        }

        protected void Application_BeginRequest(object sender, System.EventArgs e)
        {

        }

        protected void Application_AuthenticateRequest(object sender, System.EventArgs e)
        {

        }

        protected void Application_Error(object sender, System.EventArgs e)
        {

        }

        protected void Session_End(object sender, System.EventArgs e)
        {

        }

        protected void Application_End(object sender, System.EventArgs e)
        {

        }


    }


}

or you can add entries to Web.config if you don't own the application source-code:

      <httpModules>
        <add name="RequestLanguageChanger" type= "libRequestLanguageChanger.RequestLanguageChanger, libRequestLanguageChanger" />
      </httpModules>
    </system.web>

  <system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>

    <modules runAllManagedModulesForAllRequests="true">
      <add name="RequestLanguageChanger" type="libRequestLanguageChanger.RequestLanguageChanger, libRequestLanguageChanger" />
    </modules>
  </system.webServer>
</configuration>

The entry in system.webServer is for IIS7+, the other in system.web is for IIS 6.
Note that you need to set runAllManagedModulesForAllRequests to true, for that it works properly.

The string in type is in the format "Namespace.Class, Assembly". Note that if you write your assembly in VB.NET instead of C#, VB creates a default-Namespace for each project, so your string will look like

"[DefaultNameSpace.Namespace].Class, Assembly"

If you want to avoid this problem, write the DLL in C#.

Stefan Steiger
  • 78,642
  • 66
  • 377
  • 442
  • I think you might want to remove 'vmswisslife' and 'vmraiffeisen' from the answer so it won't get false correlations. – quetzalcoatl Dec 29 '19 at 22:21
  • @quetzalcoatl: I left them there as an example, it's not an oversight, it's not in any way confidential. But true, maybe better remove them. Done. – Stefan Steiger Dec 30 '19 at 09:45
  • "avoid adding allow-from when the browser is Chrome (it produces an error on the debug - console, which can quickly fill the console up, or make the application slow)" - Has this perhaps changed in recent versions of Chrome? I see no such "errors" in the console on Chrome? – MrWhite Nov 27 '20 at 17:05
  • ...further to my comment above. I only see "errors" reported in Chrome's console for wholly invalid directives of the `X-Frame-Options` header. `ALLOW-FROM` and even `ALLOWALL` (strictly invalid, but in "common use") do not result in "errors", but `X-Frame-Options THIS-IS-INVALID` does (even though I assume all are ignored by Chrome). I'm wondering if I'm missing a trick to increase the sensitivity of the debug/error reporting in the console - but I don't think so? Using Chrome 86. – MrWhite Nov 28 '20 at 01:46
7

How about an approach that not only allows multiple domains, but allows dynamic domains.

The use case here is with a Sharepoint app part which loads our site inside of Sharepoint via an iframe. The problem is that sharepoint has dynamic subdomains such as https://yoursite.sharepoint.com. So for IE, we need to specify ALLOW-FROM https://.sharepoint.com

Tricky business, but we can get it done knowing two facts:

  1. When an iframe loads, it only validates the X-Frame-Options on the first request. Once the iframe is loaded, you can navigate within the iframe and the header isn't checked on subsequent requests.

  2. Also, when an iframe is loaded, the HTTP referer is the parent iframe url.

You can leverage these two facts server side. In ruby, I'm using the following code:

  uri = URI.parse(request.referer)
  if uri.host.match(/\.sharepoint\.com$/)
    url = "https://#{uri.host}"
    response.headers['X-Frame-Options'] = "ALLOW-FROM #{url}"
  end

Here we can dynamically allow domains based upon the parent domain. In this case, we ensure that the host ends in sharepoint.com keeping our site safe from clickjacking.

I'd love to hear feedback on this approach.

Peter P.
  • 3,221
  • 2
  • 25
  • 31
  • 2
    Caution: this breaks if the host is "fakesharepoint.com". The regex should be: `/\.sharepoint\.com$/` – nitsas Dec 11 '15 at 13:48
  • @StefanSteiger that's right, but Chrome also doesn't experience this issue. Chrome and more standards compliant browsers follow the newer Content Security Policy (CSP) model. – Peter P. Apr 10 '17 at 20:21
4

As per the MDN Specifications, X-Frame-Options: ALLOW-FROM is not supported in Chrome and support is unknown in Edge and Opera.

Content-Security-Policy: frame-ancestors overrides X-Frame-Options (as per this W3 spec), but frame-ancestors has limited compatibility. As per these MDN Specs, it's not supported in IE or Edge.

Andrew
  • 2,368
  • 1
  • 23
  • 30
2

The RFC for the HTTP Header Field X-Frame-Options states that the "ALLOW-FROM" field in the X-Frame-Options header value can only contain one domain. Multiple domains are not allowed.

The RFC suggests a work around for this problem. The solution is to specify the domain name as a url parameter in the iframe src url. The server that hosts the iframe src url can then check the domain name given in the url parameters. If the domain name matches a list of valid domain names, then the server can send the X-Frame-Options header with the value: "ALLOW-FROM domain-name", where domain name is the name of the domain that is trying to embed the remote content. If the domain name is not given or is not valid, then the X-Frame-Options header can be sent with the value: "deny".

Community
  • 1
  • 1
Nadir Latif
  • 3,690
  • 1
  • 15
  • 24
2

Strictly speaking no, you cant.

You can however specify X-Frame-Options: mysite.com and therefore allow subdomain1.mysite.com and subdomain2.mysite.com. But yes, that's still one domain. There happens to be some workaround for this, but I think it's easiest to read that directly at the RFC specs: https://www.rfc-editor.org/rfc/rfc7034

It's also worth to point out that the Content-Security-Policy (CSP) header's frame-ancestor directive obsoletes X-Frame-Options. Read more here.

Community
  • 1
  • 1
Jim Aho
  • 9,932
  • 15
  • 56
  • 87
0

I had to add X-Frame-Options for IE and Content-Security-Policy for other browsers. So i did something like following.

if allowed_domains.present?
  request_host = URI.parse(request.referer)
  _domain = allowed_domains.split(" ").include?(request_host.host) ? "#{request_host.scheme}://#{request_host.host}" : app_host
  response.headers['Content-Security-Policy'] = "frame-ancestors #{_domain}"
  response.headers['X-Frame-Options'] = "ALLOW-FROM #{_domain}"
else
  response.headers.except! 'X-Frame-Options'
end
jbmyid
  • 1,985
  • 19
  • 22
0

The rule that worked for me for multiple domains and sub-domains for Apache and .htaccess is as below:

Header always append Content-Security-Policy "frame-ancestors 'self' site1 site2;"

Example:

The below rule will allow only yoursite (self), https://example1.com/ and https://example2.com to place iFrame of yoursite.

Header always append Content-Security-Policy "frame-ancestors 'self' https://example1.com/ https://example.com;"

Here is the reference link

Imran Zahoor
  • 2,521
  • 1
  • 28
  • 38
  • Please note that newbedev is a Stack Overflow scraper; don't link to it. Instead, google the text or title (optionally with `site:stackoverflow.com`) and find the correct on-site link, instead of giving scrapers more traffic that they don't deserve. – Zoe Dec 26 '21 at 15:51
  • I don't know what edit you made in the answer here, can you please specificy @Zoe? – Imran Zahoor Dec 27 '21 at 11:27
  • 1
    It's all available in [the edit history](https://stackoverflow.com/posts/70425889/revisions) – Zoe Dec 27 '21 at 15:18
0

put the same code in next.config.js

module.exports = {  
    async headers() {
        return [
            {
                source: '/((?!embed).*)',
                headers: [
                    {
                        key: 'X-Frame-Options',
                        value: 'SAMEORIGIN',
                    }
                ]
            }
        ];
    }
}
user3300505
  • 21
  • 1
  • 3
-1

Not exactly the same, but could work for some cases: there is another option ALLOWALL which will effectively remove the restriction, which might be a nice thing for testing/pre-production environments

Willyfrog
  • 475
  • 1
  • 5
  • 12
-4

One possible workaround would be using a "frame-breaker" script as described here

You just need to alter the "if" statement to check for your allowed domains.

   if (self === top) {
       var antiClickjack = document.getElementById("antiClickjack");
       antiClickjack.parentNode.removeChild(antiClickjack);
   } else {
       //your domain check goes here
       if(top.location.host != "allowed.domain1.com" && top.location.host == "allowed.domain2.com")
         top.location = self.location;
   }

This workaround would be safe, I think. because with javascript not enabled you will have no security concern about a malicious website framing your page.

SinaX
  • 1
  • 3
-9

YES. This method allowed multiple domain.

VB.NET

response.headers.add("X-Frame-Options", "ALLOW-FROM " & request.urlreferer.tostring())
  • 9
    This seems to defeat the purpose of X-Frame-Options as it allows any site to frame. – Andrey Shchekin Jun 19 '15 at 08:35
  • 6
    This answer seems like it could be a good base as a solution but it needs extra logic so that it only executes this code if the request.urlreferer.tostring() is one of the origins you wish to allow. – Zergleb Oct 09 '15 at 19:28
  • If you are doing this, why are you even using X-Frame-Options Header... just ignore it – vs4vijay Dec 26 '16 at 07:46