Skip to main content

3 Solutions to Break iframe Security Restrictions

Free2019-12-22#Node#Solution#frame-src与frame-ancestors#iframe安全限制#Refused to display x in a frame#内容安全策略#x-frame-options

Either walk in, or go around

I. Starting from iframe

Using iframe can embed third-party pages, for example:

<iframe style="width: 800px; height: 600px;" src="https://www.baidu.com"/>

However, not all third-party pages can be embedded through iframe:

<iframe style="width: 800px; height: 600px;" src="https://github.com/join"/>

Github login page doesn't display nicely in the iframe like Baidu homepage, and outputs an error line in the Console panel:

Refused to display 'https://github.com/join' in a frame because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'none'".

Why is this?

II. Clickjacking and Security Policies

Yes, prohibiting pages from being loaded in iframe is mainly to prevent clickjacking (Clickjacking):

Specifically, for clickjacking, there are mainly 3 countermeasures:

  • CSP (Content Security Policy)

  • X-Frame-Options

  • framekiller

Server declares CSP and X-Frame-Options by setting HTTP response headers, for example:

# Not allowed to be embedded, including <frame>, <iframe>, <object>, <embed> and <applet>
Content-Security-Policy: frame-ancestors 'none'
# Only allowed to be embedded by same-origin pages
Content-Security-Policy: frame-ancestors 'self'
# Only allowed to be embedded by pages within whitelist
Content-Security-Policy: frame-ancestors www.example.com

# Not allowed to be embedded, including <frame>, <iframe>, <embed> and <object>
X-Frame-Options: deny
# Only allowed to be embedded by same-origin pages
X-Frame-Options: sameorigin
# (Deprecated) Only allowed to be embedded by pages within whitelist
X-Frame-Options: allow-from www.example.com

P.S. Same-origin means protocol, domain name, and port number are all exactly the same, see Same-origin policy

P.S. Additionally, there's a frame-src that looks very similar to frame-ancestors, but they have opposite functions, the latter is used to restrict the content sources that <iframe> and <frame> in the current page can load

As for framekiller, it's executing a piece of JavaScript on the client side, thereby turning the tables:

// Original version
<script>
if(top != self) top.location.replace(location);
</script>

// Enhanced version
<style> html{display:none;} </style>
<script>
if(self == top) {
  document.documentElement.style.display = 'block';
} else {
  top.location = self.location;
}
</script>

And Github login page simultaneously sets CSP and X-Frame-Options response headers:

Content-Security-Policy: frame-ancestors 'none';
X-Frame-Options: deny

Therefore cannot be embedded through iframe, so, is there a way to break these restrictions?

III. Ideas

Since main restrictions come from HTTP response headers, then there are at least two approaches:

  • Tamper with response headers to satisfy iframe security restrictions

  • Don't directly load source content, bypass iframe security restrictions

At any link before resource response reaches the endpoint, intercept and change CSP and X-Frame-Options, such as intercepting and tampering when client receives response, or forwarding and tampering through proxy service

And another approach is very interesting, using Chrome Headless to load source content, convert to screenshot and display in iframe. For example Browser Preview for VS Code:

Browser Preview is powered by Chrome Headless, and works by starting a headless Chrome instance in a new process. This enables a secure way to render web content inside VS Code, and enables interesting features such as in-editor debugging and more!

That is to say, load page normally through Chrome, then screenshot content and put into iframe, thus not subject to above security policy restrictions (including framekiller). But this solution is also not perfect, exists other problems:

IV. Solutions

Client-side Interception

Service Worker

To intercept and tamper HTTP responses, the first thing that comes to mind is naturally Service Worker (a kind of [Web Worker](/articles/理解 web-workers/)):

A service worker is an event-driven worker registered against an origin and a path. It takes the form of a JavaScript file that can control the web-page/site that it is associated with, intercepting and modifying navigation and resource requests.

(Excerpted from Service Worker API)

After registering Service Worker can intercept and modify resource requests, for example:

// 1.Register Service Worker
navigator.serviceWorker.register('./sw-proxy.js');

// 2.Intercept requests (sw-proxy.js)
self.addEventListener('fetch', async (event) => {
  const {request} = event;
  let response = await fetch(request);
  // 3.Reconstruct Response
  response = new Response(response.body, response)
  // 4.Tamper response headers
  response.headers.delete('Content-Security-Policy');
  response.headers.delete('X-Frame-Options');

  event.respondWith(Promise.resolve(originalResponse));
});

Note, Fetch Response doesn't allow directly modifying request headers, need to reconstruct a new one, see Alter Headers

P.S. For complete implementation case, can refer to DannyMoerkerke/sw-proxy

WebRequest

If in Electron environment, can also use WebRequest API to intercept and tamper responses:

const { session } = require('electron')

session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  callback({
    responseHeaders: {
      ...details.responseHeaders,
      'Content-Security-Policy': ['default-src \'none\'']
    }
  })
})

(Excerpted from CSP HTTP Header)

But similar to Service Worker, WebRequest also depends on client environment, and for security considerations, these capabilities will be disabled in some environments, at this time need to find a way out from server side, such as forwarding through proxy service

Proxy Service Forwarding

Basic idea is to forward source requests and responses through proxy service, modify response headers or even response body during forwarding process

Specific implementation, divided into 2 steps:

  • Create proxy service, tamper response header fields

  • Client requests proxy service

Taking HTTPS as example, proxy service simple implementation as follows:

const https = require("https");
const querystring = require("querystring");
const url = require("url");

const port = 10101;
// 1.Create proxy service
https.createServer(onRequest).listen(port);

function onRequest(req, res) {
  const originUrl = url.parse(req.url);
  const qs = querystring.parse(originUrl.query);
  const targetUrl = qs["target"];
  const target = url.parse(targetUrl);

  const options = {
    hostname: target.hostname,
    port: 80,
    path: url.format(target),
    method: "GET"
  };

  // 2.Forward request
  const proxy = https.request(options, _res => {
    // 3.Modify response headers
    const fieldsToRemove = ["x-frame-options", "content-security-policy"];
    Object.keys(_res.headers).forEach(field => {
      if (!fieldsToRemove.includes(field.toLocaleLowerCase())) {
        res.setHeader(field, _res.headers[field]);
      }
    });
    _res.pipe(res, {
      end: true
    });
  });
  req.pipe(proxy, {
    end: true
  });
}

Client iframe no longer directly requests source resources, but goes through proxy service to fetch:

<iframe style="width: 400px; height: 300px;" src="http://localhost:10101/?target=https%3A%2F%2Fgithub.com%2Fjoin"/>

In this way, Github login page can display nicely in the iframe:

[caption id="attachment_2078" align="alignnone" width="924"]iframe github login iframe github login[/caption]

References

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment