「データを送信しません」という宣言を、CSPとNetwork実測で検証できる形へ近づける方法を整理します。外部画像の通信経路を実際に見つけて塞いだ事例と、default-srcだけでは閉じない経路も含めて解説します。
目次(10)
「送信しません」は、宣言だけでは検証しにくい
oka-project.com/toolsで公開しているMarkdown ViewerとYomuは、入力された本文やファイルを、プレビュー・編集・変換のために外部サーバーへ送らない方針で作っています。
ただ「送信しません」と書くだけでは、利用者から見れば運営者の約束です。
そこで、実装をブラウザ内処理にするだけでなく、Content Security Policy(CSP)で不要な通信経路を狭め、開発者ツールのNetworkタブでも実際の通信を確認できる構成にしています。
重要なのは、CSPがあればあらゆる送信が不可能になる、という意味ではないことです。CSPには制御対象ごとのディレクティブがあり、default-srcへフォールバックするものと、しないものがあります。
default-src 'none'を出発点に、必要な経路だけ開く
ブラウザ完結ツールでは、原則禁止から始めて必要なリソースだけを許可する考え方を採っています。
default-src 'none';
script-src 'self';
style-src 'self';
font-src 'self';
img-src 'self' data:;
default-src 'none'は、未指定のfetchディレクティブに対する強い既定値になります。
たとえばconnect-srcを別途指定していなければ、default-srcへフォールバックします。そのため、ページ内スクリプトからのfetch()、XMLHttpRequest、WebSocket、EventSource、sendBeacon()などの接続は許可されません。
ここで重要なのは、制約を強制するのがページ内のJavaScriptではなくブラウザだという点です。許可されていない接続をスクリプトが試みても、CSPによってブロックされます。
ただし「明示していないものは全部禁止」ではない
以前は、default-src 'none'を基点にすれば「明示的に許可していないものはすべて禁止」と説明していました。これは広すぎる表現でした。
W3CのCSP仕様とMDNでは、default-srcはfetchディレクティブのフォールバックとして働きます。一方、HTMLフォームの送信先を制御するform-actionにはフォールバックしません。
つまり、
Content-Security-Policy: default-src 'none'
だけでは、フォーム送信先まで自動的に'none'になるわけではありません。
フォーム送信も許可しない設計なら、たとえば次を明示する必要があります。
form-action 'none';
同様に、<base>要素で指定できるURLを制限するbase-uriもdefault-srcへフォールバックしません。厳しく閉じるなら、用途に応じて次のように明示します。
base-uri 'none';
そのため、正確には「CSPで送信できない」と一括りにするのではなく、どのディレクティブで、どの通信・読み込み経路を閉じているかを見る必要があります。
実際に1つ、外部通信の穴が残っていた
この考え方が机上の話ではないと分かったのが、公開後の監査です。
当初は画像についてimg-src ... https:と広く許可しており、任意のHTTPSサイトから画像を読み込める状態でした。
Markdownを表示するツールでは、これは無視できません。
悪意あるMarkdownに次の1行があるとします。

外部画像をそのまま表示すれば、ブラウザはattacker.exampleへリクエストします。本文をPOSTしていなくても、外部通信が発生し、接続先には通常のHTTP通信に伴う情報が渡ります。
現在公開しているMarkdown ViewerとYomuでは、この種の外部画像通信を既定で許可しない方針にしています。Markdown内に外部画像URLが書かれていても、プレビューのために相手先へ自動接続しない設計です。書き出したMarkdownやHTMLには元のURLが残るため、公開先での表示は公開先側の処理に従います。
対処は二段にしました。
- CSPの
img-srcを'self' data:へ絞り、外部画像取得をブラウザ側で制限する - Markdownの表示処理でも外部画像URLをそのまま読み込まず、遮断理由が分かる表示へ置き換える
CSPだけ、アプリ側の処理だけのどちらかに寄せず、二重に制限する形です。
Networkタブでは「ページ表示」と「原稿処理」を分けて見る
ブラウザ完結ツールでも、ページを開くときにはHTML、JavaScript、CSS等を取得するため通常のWeb通信が発生します。
確認したいのは「通信が1件もないか」ではなく、Markdownを入力・プレビュー・変換したときに、原稿を伴う追加通信が発生するかです。
ChromeやEdgeなら、次のように確認できます。
- 開発者ツールのNetworkタブを開く
- ページの読み込みが終わったらログをクリアする
- 架空のテスト文字列をMarkdownへ入力する
- プレビューや変換など、確認したい操作を行う
- 新しいリクエストの送信先・Method・Payloadを確認する
追加通信がなければ、少なくともその操作・その時点の実装では、Networkタブ上に原稿送信を確認できなかったと言えます。
これは実測として有用ですが、テストしていない機能や将来の変更まで保証するものではありません。
宣言・実装・CSP・実測を分けて考える
ブラウザ完結ツールの「送信しない」を確認するときは、1つの仕組みにすべてを背負わせない方が正確です。
- 方針:入力本文を変換サーバーへ送らないと明示する
- 実装:Markdownの解析・変換をブラウザ内で行う
- CSP:不要な接続先・画像取得先・フォーム送信先などを制限する
- 実測:Networkタブで実際の操作時に何が通信されるかを見る
CSPはこの中でも重要な一層ですが、端末全体の安全性を保証する仕組みではありません。ブラウザ拡張機能、OS、共有端末、利用者が明示的に使うAI機能などは別に考える必要があります。
次のツールでも同じ問題を起こさないための回帰チェック
今回のCSP対応で残したい教訓は「特定のヘッダーを一度設定した」ことではなく、新機能を足したときにも外部通信の経路を意図的にレビューできる状態です。
| 変更した機能 | 追加で確認するポリシー・動作 | テストに使う入力 |
|---|---|---|
| プレビューへ画像表示を追加 | img-srcと画像のリクエスト先 | 外部ドメインのダミー画像URL |
| エクスポート処理を追加 | connect-srcと実際の通信 | 個人情報を含まない固定原稿 |
| 外部フォント/ライブラリを追加 | 対応するロード元と必要性 | ページ初回ロードのNetwork記録 |
| ファイル選択とダウンロードを変更 | 元のファイルがどこへ移動するか | 空のテストファイル/公開ダミーデータ |
| ブラウザへの保存を追加 | 保存先・同期と再読込の挙動 | テスト用の文字列 |
CSPだけですべてのプライバシーを保証できるわけではありません。実装・HTTPヘッダー・実際の操作時の通信を、変更のたびに組み合わせて確認する必要があります。
学んだこと
- 「送信しない」という宣言と、ブラウザが強制する制約は分ける。 両方がある方が利用者は検証しやすい
default-src 'none'は強い出発点だが万能ではない。form-actionのように別途明示が必要なディレクティブがある- 読み込みも通信になり得る。 Markdownの外部画像を許しただけでも外部リクエストは発生する
- 実測は操作単位で行う。 ページ読み込みの通信と、本文処理に伴う通信を分けて見る
- 制限は隠さず見せる。 外部画像を表示しない理由などをUI上で説明すれば、制約を信頼材料にできる
この考え方は、今後追加するブラウザ完結ツールにも適用します。「送信できない」と大きく一括りにするのではなく、何をブラウザ内で処理し、どの経路をCSPで閉じ、何を実測できるかを具体的に示す方針です。
現在この方針を適用しているツール
Markdown Viewer — Markdownを開いて確認し、PDFやHTMLへ持っていく
Yomu — Markdownを編集し、note・WordPress・はてなブログ・Word・PDF・HTMLへ変換する
Karuku — 画像の圧縮・リサイズをブラウザ内で行う。画像データはサーバー処理へ送らず、WASMコーデックも端末側で動かす
ツールごとに機能は異なりますが、「処理対象のデータを必要以上に外へ出さない」「不要な通信経路はCSPで狭める」「利用者が挙動を確認できるようにする」という基準は共通です。