X-Frame-Options: DENY/SAMEORIGINでも iframe で表示する回避策

## Overview

標題の通り、`X-Frame-Options: DENY` or `SAMEORIGIN` でも iframe で表示する JS module を使ってみたのでメモ。

※ 記事内の herokuapp は現在稼働しておりません

## X-Frame-Bypass

これを使えばできる（すべてのページが表示できるわけではありません）。

https://github.com/niutech/x-frame-bypass

### Note that:

* すべてのページが表示できるわけではありません。
    * 例えば CAPTCHA が入ってたりするとダメです。
    * 踏み台にしている proxy から html を取得してこれないとダメみたいです。

## How to use?

使い方は [README](https://github.com/niutech/x-frame-bypass/blob/master/README.md) に書いてある通りとてもシンプル。

JS module を読み込んで iframe タグに `is="x-frame-bypass"` を付けるだけ。

## Try

`X-Frame-Options: DENY` / `SAMEORIGIN` を返す Heroku 上のサーバで試してみる。

* https://bypass-test-001.herokuapp.com/deny
    * `X-Frame-Options: DENY`
* https://bypass-test-001.herokuapp.com/same
    * `X-Frame-Options: SAMEORIGIN`

以下の２つの html でブラウザ表示、およびコンソールログを確認する。

※ 上記 herokuapp は現在稼働しておりません

1.通常の iframe で書いた html

```html:iframe_normal.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Normal iframe</title>
    <link rel="stylesheet" href="index.css">
  </head>
  <body>
    <div class="main">
      <iframe id="main-frame" frameborder="no"
        src="https://bypass-test-001.herokuapp.com/same">
      </iframe>
    </div>
  </body>
</html>
```

2.iframe w/ X-Frame-Bypass で書いた html

```html:iframe_baypass.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>iframe with x-frame-bypass</title>
    <link rel="stylesheet" href="index.css">
    <script type="module" src="https://unpkg.com/x-frame-bypass"></script>
  </head>
  <body>
    <div class="main">
      <iframe id="main-frame" frameborder="no" is="x-frame-bypass"
        src="https://bypass-test-001.herokuapp.com/same">
      </iframe>
    </div>
  </body>
</html>
```

## X-Frame-Options: DENY

まずは X-Frame-Options ヘッダを確認

DENY になっている

```
$ curl -D - -s https://bypass-test-001.herokuapp.com/deny
HTTP/1.1 200 OK
Server: Cowboy
Date: Fri, 01 Jan 2021 09:09:04 GMT
Connection: keep-alive
X-Frame-Options: DENY
Content-Length: 93
Content-Type: text/html; charset=utf-8
Via: 1.1 vegur


    <pre>
    X-Frame-Options': 'DENY'
    Do not show this page via iframe.
    </pre>
```

### 結果: 通常の iframe で書いた html

ブラウザ

![result_deny_normal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145721/9d93e5d6-5d73-1623-f1f2-8689f31fe1e1.png)

コンソールログ

*"Refused to display 'https://bypass-test-001.herokuapp.com/deny' in a frame because it set 'X-Frame-Options' to 'deny'."*

![log_deny_normal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145721/2207c61b-aed6-b71c-c014-1caf4323fe48.png)


### 結果: iframe w/ X-Frame-Bypass で書いた html

ブラウザ

表示できた。

![result_deny_bypass.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145721/9dfc2537-8f4d-4afd-817a-c367293b7cc1.png)

コンソールログ

X-Frame-Bypass が動いているのがわかります。

エラーは出ますが中でゴニョゴニョやっているんでしょう。最終的に表示できました。

![log_deny_bypass.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145721/ff230c7d-e46c-79c4-1de4-93610fe51098.png)


## X-Frame-Options: SAMEORIGIN

次は `X-Frame-Options: SAMEORIGIN` で試してみる。

ヘッダを確認

```
$ curl -D - -s https://bypass-test-001.herokuapp.com/same
HTTP/1.1 200 OK
Server: Cowboy
Date: Fri, 01 Jan 2021 09:09:10 GMT
Connection: keep-alive
X-Frame-Options: SAMEORIGIN
Content-Length: 119
Content-Type: text/html; charset=utf-8
Via: 1.1 vegur


    <pre>
    X-Frame-Options': 'SAMEORIGIN'
    Do not show this page via iframe without same-origin.
    </pre>
```

### 結果: 通常の iframe で書いた html

ブラウザの表示結果は DENY の場合と同じなので省略します。

コンソールログ

*"Refused to display 'https://bypass-test-001.herokuapp.com/same' in a frame because it set 'X-Frame-Options' to 'sameorigin'."*

![log_sameorigin_normal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145721/63c5f43f-29fd-53a5-5ec6-542b4106eeef.png)



### 結果: iframe w/ X-Frame-Bypass で書いた html

ブラウザ

こちらも表示できますね。

![result_sameorigin_bypass.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145721/620b1f06-0a29-5543-4183-3f0506cc2d76.png)

コンソールログ

![log_sameorigin_bypass.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145721/4a66971b-d266-2cca-8158-c4c3b1fd7fa7.png)


## 補足1: Vue で使う場合

これが何気に苦労しました。[npm でパッケージ化されている](https://www.npmjs.com/package/x-frame-bypass)のですが、どう書いたら動くのかわからず、、

結局 [vue-head](https://www.npmjs.com/package/vue-head) で module を読み込んで使うことにしました。

無理やり感のあるコードですが、参考まで。

```vuejs:IframeTest.vue
<template>
  <div class="iframe-test">
    <div class="test-frame">
    </div>
  </div>
</template>

<script>
export default {
  name: 'IframeTest',
  data () {
    return {
      iframeId: 'test-frame-id'
    }
  },
  head: {
    script: [
      {
        type: 'module',
        src: 'https://unpkg.com/x-frame-bypass'
      }
    ]
  },
  created: async function () {
    if (document.getElementById(this.iframeId)) {
      const iframe = document.getElementById(this.iframeId)
      iframe.remove()
    }
    const div = await document.getElementsByClassName('test-frame')
    div[0].insertAdjacentHTML(
      'afterbegin', this.getFrameHtml())
    await this.$nextTick()
  },
  methods: {
    getFrameHtml: function () {
      return `
        <iframe
          id="test-frame-id"
          frameborder="no"
          is="x-frame-bypass"
          src="https://bypass-test-001.herokuapp.com/deny">
        </iframe>`
    }
  }
}
</script>

<style scoped>
</style>
```

## 補足2: 少しだけモジュールの中身を確認

この API を使っている模様（[他にも２つ proxy がある](https://github.com/niutech/x-frame-bypass/blob/master/x-frame-bypass.js#L71)）。

https://cors-anywhere.herokuapp.com/

```
This API enables cross-origin requests to anywhere.

Usage:

/               Shows help
/iscorsneeded   This is the only resource on this host which is served without CORS headers.
/<url>          Create a request to <url>, and includes CORS headers in the response.

If the protocol is omitted, it defaults to http (https if port 443 is specified).

Cookies are disabled and stripped from requests.

Redirects are automatically followed. For debugging purposes, each followed redirect results
in the addition of a X-CORS-Redirect-n header, where n starts at 1. These headers are not
accessible by the XMLHttpRequest API.
After 5 redirects, redirects are not followed any more. The redirect response is sent back
to the browser, which can choose to follow the redirect (handled automatically by the browser).

The requested URL is available in the X-Request-URL response header.
The final URL, after following all redirects, is available in the X-Final-URL response header.


To prevent the use of the proxy for casual browsing, the API requires either the Origin
or the X-Requested-With header to be set. To avoid unnecessary preflight (OPTIONS) requests,
it's recommended to not manually set these headers in your code.


Demo          :   https://robwu.nl/cors-anywhere.html
Source code   :   https://github.com/Rob--W/cors-anywhere/
Documentation :   https://github.com/Rob--W/cors-anywhere/#documentation
```

こいつで iframe src に指定した URL の html を取ってこれれば表示できるんですね。多分。

おわり。