メインコンテンツへ移動

iframe のセキュリティ制限を打破する 3 つの方案

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

要么走进来,要么绕过去

一.iframe から話す

iframe を利用して第三者ページを埋め込むことができます。例えば:

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

しかし、すべての第三者ページが iframe で埋め込めるわけではありません:

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

Github ログインページは百度ホームページのように素直に iframe 内に表示されず、Console パネルにエラー行を出力しました:

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

これはなぜでしょうか?

二.クリックジャッキングとセキュリティポリシー

そうです、ページが iframe 内にロードされることを禁止するのは主にクリックジャッキング(Clickjacking)を防止するためです:

具体的には、クリックジャッキングに対して、主に 3 つの対策措置があります:

  • CSP(Content Security Policy、つまりコンテンツセキュリティポリシー)

  • X-Frame-Options

  • framekiller

サーバー側は HTTP 応答ヘッダーを設定して CSP と X-Frame-Options を宣言します。例えば:

# 埋め込みを許可しない。<frame>, <iframe>, <object>, <embed> と <applet> を含む
Content-Security-Policy: frame-ancestors 'none'
# 同源のページにのみ埋め込みを許可
Content-Security-Policy: frame-ancestors 'self'
# ホワイトリスト内のページにのみ埋め込みを許可
Content-Security-Policy: frame-ancestors www.example.com

# 埋め込みを許可しない。<frame>, <iframe>, <embed> と <object> を含む
X-Frame-Options: deny
# 同源のページにのみ埋め込みを許可
X-Frame-Options: sameorigin
# (廃止済み)ホワイトリスト内のページにのみ埋め込みを許可
X-Frame-Options: allow-from www.example.com

P.S.同源とはプロトコル、ドメイン名、ポート番号がすべて完全に同じことを指します。Same-origin policy を参照

P.S. さらに、frame-ancestors と非常によく似た frame-src がありますが、二者の作用は反対で、後者は現在のページ中の <iframe><frame> がロードできるコンテンツソースを制限するために使用されます

framekiller については、クライアント側で JavaScript を実行して、反客为主します:

// 原版
<script>
if(top != self) top.location.replace(location);
</script>

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

Github ログインページは、CSP と X-Frame-Options 応答ヘッダーを同時に設定しています:

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

したがって iframe で埋め込むことができません。では、これらの制限を打破する方法はありますか?

三.思路

主要な制限が HTTP 応答ヘッダーから来るのであれば、少なくとも 2 つの思路があります:

  • 応答ヘッダーを改ざんして、iframe セキュリティ制限を満たすようにする

  • ソースコンテンツを直接ロードせず、iframe セキュリティ制限を迂回する

リソース応答が終点に到達する前の任意の環節で、拦截して CSP と X-Frame-Options を改掉します。例えばクライアント側で応答を受け取った時に拦截改ざんするか、または代理サービスで転送改ざんします

もう一つの思路は非常に面白く、Chrome Headless を借助してソースコンテンツをロードし、スクリーンショットに変換して iframe 内に表示します。例えば 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!

つまり、Chrome で正常にページをロードし、その後コンテンツをスクリーンショットして iframe 内に入れるため、上記(framekiller を含む)セキュリティポリシーの制限を受けません。しかし、この方案も完璧ではなく、別の問題が存在します:

四.解決方案

クライアント側拦截

Service Worker

HTTP 応答を拦截改ざんするには、まず思い浮かぶのは当然 Service Worker(一種の [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.

Service Worker API から引用)

Service Worker を登録した後、リソースリクエストを拦截して修改できます。例えば:

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

// 2. リクエストを拦截(sw-proxy.js)
self.addEventListener('fetch', async (event) => {
  const {request} = event;
  let response = await fetch(request);
  // 3.Response を再構築
  response = new Response(response.body, response)
  // 4. 応答ヘッダーを改ざん
  response.headers.delete('Content-Security-Policy');
  response.headers.delete('X-Frame-Options');

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

注意、Fetch Response は直接リクエストヘッダーを修改することを許可されていません。再構築する必要がありますAlter Headers を参照

P.S. 完全な実装案例は、DannyMoerkerke/sw-proxy を参照できます

WebRequest

Electron 環境であれば、WebRequest API を借助して応答を拦截して改ざんすることもできます:

const { session } = require('electron')

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

CSP HTTP Header から引用)

しかし Service Worker と同様に、WebRequest もクライアント側環境に依存します。安全性の考慮から、これらの能力はいくつかの環境で無効にされる可能性があり、この時はサーバー側から出路を探す必要があります。例えば代理サービスを通過して転送します

代理サービス転送

基本的な思路は代理サービスを通過してソースリクエストと応答を転送し、転送過程中に応答ヘッダーさらには応答体を修改します

具体的実装では、2 ステップに分かれます:

  • 代理サービスを作成し、応答ヘッダーフィールドを改ざん

  • クライアント側が代理サービスにリクエスト

HTTPS を例にすると、代理サービスの簡単な実装は以下の通り:

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

const port = 10101;
// 1. 代理サービスを作成
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. 代发リクエスト
  const proxy = https.request(options, _res => {
    // 3. 応答ヘッダーを修改
    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
  });
}

クライアント側 iframe は直接ソースリソースにリクエストせず、代理サービスを通過して取得します:

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

このようにして、Github ログインページは iframe 内で素直に表示できます:

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

参考資料

コメント

コメントはまだありません

コメントを書く