一.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
サーバー側は 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[/caption]
コメントはまだありません