Cloudflare における Mixed Content エラーへの対応

# Mixed Content エラーとは
要は HTTPS ページの中に HTTP が混ざっているとブラウザでエラーが出ます。

エラーを出さないためには、様々なリソースのリンクを HTTPS で統一する必要があります。

>- [混在コンテンツ - ウェブセキュリティ | MDN](https://developer.mozilla.org/ja/docs/Web/Security/Mixed_content)
>
>**混在コンテンツ**
ユーザが HTTPS を通じてページにアクセスすると、ユーザとウェブサーバとの接続は TLS で暗号化され、盗聴や中間者攻撃から保護されます。HTTPS のページの中に通常の平文の HTTP で送られてくるコンテンツが含まれている場合、混在コンテンツと呼ばれます。このようなページは部分的にしか暗号化されておらず、盗聴者や中間者攻撃者が暗号化されていないコンテンツにアクセスできてしまいます。つまり、ページは安全ではありません。

>- [混合コンテンツとは？| HTTPとHTTPS | Cloudflare](https://www.cloudflare.com/ja-jp/learning/ssl/what-is-mixed-content/)
>
>**混合コンテンツとは？**
TLS（SSLも呼ばれます）を使用すると、インターネット通信が暗号化され、より安全なブラウジング体験を提供できます。TLSで暗号化されているサイトはURLに「http://」ではなく「https://」が含まれているため、ユーザーは簡単に識別することができます。ただし、場合によってはHTTPSサイトには平文のHTTPプロトコルを使用して読み込まれるいくつかの要素を含めることもできます。これは「HTTP over HTTPS」とも呼ばれる、混合コンテンツと呼ばれる状態を生み出します。
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/fa92eb8a-aa08-7a0f-6875-a09efdd552ff.png)
>
>**混合コンテンツのエラーの修正方法は？**
混合コンテンツの修正は非常に単純です。Web開発者は、ページ上のすべてのリソースがHTTPS経由でロードされるようにする必要があります。

# Mixed Content エラーへの対応
以下のようなガイドがありますが、Automatic HTTPS Rewrites と Snippets を使った例を確認します。

- リンク指定時に相対パスを使う（絶対 URL 指定に `http` を使わない）
- Automatic HTTPS Rewrites 等を使って `http` を `https` に書き換える

>- [Mixed content errors | Cloudflare SSL/TLS docs](https://developers.cloudflare.com/ssl/troubleshooting/mixed-content-errors/#_top)
>
>**解決方法**
**一般的なアドバイス**
混合コンテンツエラーを解決するには、2つの方法があります。
>
> 1. HTTPまたはHTTPSプロトコルを指定せずに、HTMLソースからすべてのリソースを読み込みます。例えば、`http://domain.com/path/to.file` の代わりに `/domain.com/path/to.file` を使用します。
> 2. コンテンツ管理システムによっては、HTTPリソースを自動的にHTTPSに書き換えるプラグインがないか確認する。Cloudflareは、Automatic HTTPS Rewritesを介してそのようなサービスを提供しています。

## Automatic HTTPS Rewrites
ゾーンごとの設定、または Configuration Rules によって、設定できます。

https://developers.cloudflare.com/ssl/edge-certificates/additional-options/automatic-https-rewrites/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/131a83be-4df3-954f-27cd-506ef2de7bf9.png)

https://developers.cloudflare.com/rules/configuration-rules/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/3cecf2c9-2ea1-8b97-535f-d780f41ee3b1.png)

以下のサイトにあるデモ用の `iframe` を拝借して、動作確認します。

https://service.jcity.com/v3/supportTool/iframeSample/

>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/887b624d-25ce-400c-d394-0cf8fc36066a.png)

このような場合には Automatic HTTPS Rewrites を使って `http` を `https` に書き換えることができます。

```shell:
# Automatic HTTPS Rewrites なし
% curl -s https://example.com/ | grep "iframe src"
<iframe src="http://asp.jcity.co.jp/FORM/?UserID=demo&formid=135" frameborder="0" width="100%" height="600" style="border:1px solid #d4d4d4;"></iframe>
# Automatic HTTPS Rewrites あり
% curl -s https://example.com/ | grep "iframe src"
<iframe src="https://asp.jcity.co.jp/FORM/?UserID=demo&formid=135" frameborder="0" width="100%" height="600" style="border:1px solid #d4d4d4;"></iframe>
```

## Snippets

### Snippets なし
ただし、以下のような場合には Automatic HTTPS Rewrites の効果がないため、Snippets が必要なことがあります。

> 一部のリソースは、サイトがブラウザで読み込まれる際に、JavaScriptやCSSによってHTTP経由で読み込まれます。そのような状況では、混合コンテンツの警告が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/0613a738-1125-3600-2704-82f537cf5d5f.png)

```mermaid:
%%{init:{'themeCSS':' g:nth-of-type(2) rect.actor { fill: orange; }; g:nth-of-type(5) rect.actor { fill: orange; };'}}%%
sequenceDiagram
    Browser->>Cloudflare: HTTP Request
    Cloudflare->>Server: HTTP Request
    Server->>Cloudflare: JSON Response<br>{"Uri":"http://iframe.src.example.com/"}
    Cloudflare->>Browser: JSON Response<br>{"Uri":"http://iframe.src.example.com/"}
    Browser-->Browser: document.createElement("iframe")<br>iframe.src = uri
    Note right of Browser: Mixed Content Error
    participant Cloudflare
```

例えば、以下のような Server としての Workers と、HTML ファイルを用意することで動作検証ができます。

```html:iframe-mixed-content.html
<html>
  <head>
    <script type="text/javascript">
      function addIframe() {
        var url = "https://return-json.example.com/";
        fetch(url)
          .then(function (data) {
            return data.json();
          })
          .then(function (json) {
            var uri = json.d.ReportLink.Uri;
            console.log(uri);

            var target = document.getElementById("targetDiv");
            var iframe = document.createElement("iframe");
            iframe.style["width"] = "100%";
            iframe.style["height"] = "600";
            iframe.style["border-style"] = "border:1px solid #d4d4d4;";
            iframe.src = uri;
            iframe.frameBorder = 0;

            target.appendChild(iframe);
          });
      }
    </script>
  </head>

  <body>
    <a href="javascript:addIframe()">Test</a>
    <div id="targetDiv"></div>
  </body>
</html>

```

<iframe src="http://asp.jcity.co.jp/FORM/?UserID=demo&formid=135" frameborder="0" width="100%" height="600" style="border:1px solid #d4d4d4;"></iframe>

```javascript:worker.js (https://return-json.example.com/)
export default {
  async fetch(request) {
    const data = {
      "d": {
        "ReportLink": {
          "Uri": "http://asp.jcity.co.jp/FORM/?UserID=demo&formid=135",
        },
      }
    };

    return Response.json(data);
  },
};
```

### Snippets あり

この場合、以下のような Snippets を設定することでクライアントブラウザ内のスクリプトで取得される `iframe.src` としての URL を `http` から `https` に書き換えることができます。

```mermaid:
%%{init:{'themeCSS':' g:nth-of-type(2) rect.actor { fill: orange; }; g:nth-of-type(5) rect.actor { fill: orange; };'}}%%
sequenceDiagram
    Browser->>Cloudflare: HTTP Request
    Cloudflare->>Server: HTTP Request
    Server->>Cloudflare: JSON Response<br>{"Uri":"http://iframe.src.example.com/"}
    Cloudflare-->Cloudflare: Snippets<br>(Modify JSON body)
    Cloudflare->>Browser: JSON Response<br>{"Uri":"https://iframe.src.example.com/"}
    Browser-->Browser: document.createElement("iframe")<br>iframe.src = uri
    participant Cloudflare
```


要件としては Workers でも問題ありませんが、簡易版 Workers である Snippets での設定例を確認します。

https://developers.cloudflare.com/rules/snippets/

https://developers.cloudflare.com/workers/examples/modify-response/

```javascript:snippets.js
export default {
    async fetch(request) {

        const originalResponse = await fetch(request);

        const originalBody = await originalResponse.json();
        const body = JSON.stringify({
            ...originalBody
        }).replace('http://', 'https://');
        const response = new Response(body, originalResponse);

        const contentType = originalResponse.headers.get("Content-Type");

        if (contentType.startsWith("application/json")) {
            return response;
        } else {
            return originalResponse;
        }
    },
};
```

Mixed Content エラーなく `<iframe src="https://...` として表示されることを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/4b044dc4-9c63-c81f-2958-d379aca2a664.png)

# まとめ

今回、例えば Web アプリの「絶対 URL 指定に http を使わない」等の改修が間に合わない場合に、 Cloudflare の Automatic HTTPS Rewrites や Snippets を使って `http` を `https` に書き換え、Mixed Content エラーを回避できることを確認しました。

ただ、長期的な目線では、Web アプリ側で「絶対 URL 指定に http を使わない」等の改修を反映できれば、構成上もシンプルにできるため、ベストと思います。

# 参考： Snippets と Workers の違い

https://developers.cloudflare.com/rules/snippets/

https://developers.cloudflare.com/workers/platform/limits/

|   | Snippets | Workers |
|:-:|:-:|:-:|
| Use Case | HTTP **modifications** | **Full-stack** applications |
| Runtime Support | JavaScript | WASM (JavaScript, Python, Rust and more) |
| Subrequests | **Pro**: 2 per request<br>**Business**: 3 per request<br>**Enterprise**: 5 per request | **Free**: 50 per request<br>**Standard**: 1,000 per request |
| Maximum Execution Time | 5 ms | **Free**: 10 ms<br>**Standard**: 30 seconds |
| Environment Variables | :x:  | :white_check_mark:​​ |
| Bindings (KV, DO, R2, D1, etc.) | :x: | :white_check_mark:​ |
| Wrangler CLI | :x: | :white_check_mark:​ |