Preventing Clickjacking Using Content Security Policy

I recently came across an issue with a legacy system which allowed its login page to be framed by any site. This is a problem because that login page needs to be framed by other pages that reside on other subdomains of the same site.

Framing login pages is a bad practice in general. In most cases I would opt to just rewrite a site’s code and remove the need to frame login pages. However in this specific case rewriting the code for this system would have taken an extended amount of time. Further more the whole system is scheduled to be replaced within the next year. With these factors in mind, I went searching for a quick way to block unauthorized framing of the system’s login pages.

With the need to allow framing from multiple subdomains, the old best practice of using the X-Frame option of SAMEORIGIN won’t work. I did a little research and found the ALLOW-FROM option on Mozilla’s Developer Network. Some browsers such as FireFox and Internet Explorer will support the ALLOW-FROM option. This allows content to be shown from a given URL, including the use of wildcards. However in the MDN documentation it states that the ALLOW-FROM option not supported in Chrome or Safari.

After doing a bit of searching, I found comments on multiple bug trackers (1, 2, 3) that suggests Chrome and other WebKit browsers will never support this feature. Instead they support the Content Security Policy frame-ancestors options.

If you are unfamiliar with Content Security Policy see this link. The tl;dr is that Content Security Policy allows sites to restrict where content such as JavaScript, images, fonts, etc are loaded from and how they can be used within the page. In this post we will look specifically at the frame-ancestors option.

Implementing the frame-ancestors option is very simple and consists of adding a single header. The system I mentioned at the beginning of this post runs on Apache. So implementing this was as simple as adding the line below to the .htaccess file in the login page’s directory.

Header set Content-Security-Policy "frame-ancestors https://*.[appdomain].com;"

That single line will only allow subdomains of the app’s domain to frame the login page. All other pages will display a Content Security Policy violation message or a blank page depending on the browser. This assumes that the page is viewed with a browser that supports the frame-ancestors Content Security Policy option.

Before implementing this on the legacy system mentioned; I created a few test pages to see how this worked. I created a test login page at http://alec.dhuse.com/cs/csp/login.html. If you visit this page you will notice that there is a message regarding an externally loaded jQuery library. I included this as some Content Security Policies will block external JavaScript libraries and I wanted to make sure any changes I made still allowed these libraries to be loaded.

I also created a .htaccess file with a single entry to block framing on any site other then alec.dhuse.com. That line is:

Header set Content-Security-Policy "frame-ancestors http://alec.dhuse.com https://alec.dhuse.com;"

Next I created a page that frames that login page and includes an example of a div overlay that could be used to grab a users credentials. I then hosted that file on two different domains, one on the same subdomain here: http://alec.dhuse.com/cs/csp/framed.html and on the parent domain here: http://dhuse.com/cs/csp/framed.html.

If you visit the two links, you will notice that the first link hosted on the same subdomain will allow framing. You can also click the “Show login overlay” checkbox to see new text fields and login button appear over the framed page. These overlaid controls allow user entered credentials to be intercepted and then passed on to the original system. For the second link, the framed login page will not load and may even show an error message.

Last thoughts:

If you examine the code on the login page you will notice a meta tag attempting to implement the same policy supplied by the Apache headers. This was an attempt to implement the same policy without out the need to send an extra header. However, it appears that while many Content Security Polices can be implemented by this meta tag, frame-ancestors cannot be. I could not find a clear reason as to why, other than that it’s not in the spec.

Leave a Reply

Your email address will not be published. Required fields are marked *