Have you heard of the Content Security Policy (CSP) “frame-ancestors” directive? It is a newer alternative to the X-Frame-Options header, which offers better control and broad, but not universal, browser support.
A Bit of History
The directive was originally proposed in the February 2014 CSP working draft. The earliest browser support by Firefox and Opera came at the end the year, followed by Chrome in early 2015. Safari and Edge implemented the directive in mid/late 2016. It’s probably no surprise, given Internet Explorer’s legacy status, that frame-ancestors support has not been added, nor is it expected.
On the other hand, the X-Frame-Options header is supported in the current version of every browser listed above, including Internet Explorer. Well, almost. Chrome does not support the ALLOW-FROM directive in X-Frame-Options. So if we are going to do anything involving other domains, we need something similar. We can stitch together a patchwork configuration involving both headers, which does something more than just allow same-origin framing.
Getting more specific, suppose our requirement is to configure the server in such a way that our pages may be framed from the site itself, as well as exactly one other site. And there should be a way to allow other sites in the future.
As for CSP, the solution is quite simple. We just use this header:
This one line shows the advantage of the newer frame-ancestors directive. If it is not immediately clear why, let us proceed to the comparable configuration for X-Frame-Options so that we cover the IE case as well.
We of course have both the ALLOW-FROM and SAMEORIGIN directives with X-Frame-Options, and that would appear to be all we need, but for reasons that are unclear, we cannot use them both at the same time. If we are going to allow framing, we must choose exactly one site or allow framing by all sites. We cannot allow our own site and another one, but no other sites.
But all is not lost. We can achieve the desired result using only web server directives and a linking convention. We will require that when https://example.com/ wants to frame our content, they must use a special subdomain to do so, e.g. if they want to frame page.html, then they must use the URL https://example.ourdomain.com/page.html.
Then we create a server configuration for example.ourdomain.com which includes this header:
X-Frame-Options: ALLOW-FROM https://www.example.com/
And, of course, the server for www.ourdomain.com will have a different header, because the site is allowed to frame itself:
Note that we are not actually duplicating the site or application here. We are instructing the same web server to add different headers depending on what hostname was supplied in the URL. On Nginx this can be done using multiple server blocks for example.
Combing CSP and X-Frame
We now have separate solutions that we need to combine together. But that is not difficult. The single-line CSP header must be split into two different variants, one for each of the server configurations we are using. Bringing everything together, the final headers:
Headers for www.ourdomain.com config:
Content-Security-Policy: frame-ancestors ‘self’;
Headers for example.ourdomain.com config:
Content-Security-Policy: frame-ancestors https://www.example.com;
X-Frame-Options: ALLOW-FROM https://www.example.com
The configuration should work in theory. But what happens in practice if the browser sees both the CSP header and the X-Frame-Options header? We tested all of the browsers discussed previously, and they all behave as expected. When same-origin is specified in the configuration, only framing from the same origin is allowed. When other-origin is specified, only framing from that other origin is allowed. Note that we did not test second or third order framing, where the content contains frames inside other frames.
It wasn’t really so complicated after all. State the intended behavior twice, once with each header. Perhaps it all could have been said better in fewer words. But until Microsoft stops shipping IE with Windows, the X-Frame-Options header still has its uses. And even when CSP frame-ancestors completely takes over, there still may be a need to separate framing contexts using different subdomains (or perhaps URL parameters), so this sort of split-domain configuration may outlive even the old header.