BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

フロントエンドのコードからの情報漏洩を防ぐための工夫

基盤チームの右京です。

昨今はフロントエンドのアプリケーションもリッチになり、ブラウザ上で実行されるコードが行うことの範囲も増えてきました。一方で多くのことを実装できてしまうのはリスクでもあり、BASE でも問題となることがあります。

その中でも「開発環境の URL」や「デバッグ機能の存在」ような環境毎に異なる情報は、特に意図せずに漏れやすいものだと考えています。これらはコードを記述する際に、実装方法を知識として知っていればその多くが回避可能です。この記事ではその実装例を解説しています。

コードから漏れる情報

例えば、次のようなコードがあるとします。

function debug() {
  // 開発環境の host であればデバッグ機能を有効にする
  return location.host === 'dev.example.com';
}

なんの変哲もないようなコードに見えますが、ブラウザ上で実行されるコードとしては非常に良くないものだと言えます。通常 JavaScript をブラウザで実行する場合、ソースコードが直接取得されるため、その内容は利用者が閲覧することできます。つまり「秘匿情報の開発環境の URL が、比較的誰でも簡単に入手できるものに記載されてしまっている」ということになります。

Minifier や Compressor を用いて難読化を行ったとしても、(このままでは)この URL の情報が消えることはありません。location.host という変数と比較をするため、"dev.example.com" は実行時まで必要なコードだからです。

防ぐにはどうするか?

殆どの場合は次のうちいずれかの方法で解決できるでしょう。

  1. 情報をサーバーサイドのプログラムから受け取る
  2. 環境毎にビルドで使用する定数を入れ替える
  3. ビルドフェーズで不要になるコードを確定して削除する

前者の 2 つは馴染みのある方法で、最後の 1 つはフロントエンド特有のものです。

1. 情報をサーバーサイドのプログラムから受け取る

HTML を配信するサーバーサイドのプログラムがある場合、環境による変化はそちらでコントロールします。今回の例では、デバッグ機能を利用したい場合にそのフラグを input で埋め込むような形にします。

<input type="hidden" id="debug" value="1">

JavsScript からはこの値を取得してデバッグ機能を有効にするかを決定しますが、このままではデバッグ機能自体は残ってしまうので、API のエンドポイント URL などの単純に値を切り替えたいものに向いているでしょう。

2. 環境毎にビルドで使用する定数を入れ替える

SPA などで HTML が静的な場合は、ビルド時に 1. と同等の置き換えを行うと良いでしょう。できることは 1. と同様なため、デバッグ機能そのものはコードとして残ってしまうことは変わりません。

環境毎に API の URL が変更されるこのようなコードがあるとします。

function baseURL() {
    return API_BASE_URL;
}

fetch(`${baseURL()}/api`)

今回は esbuild の CLI を用いてこれをビルドします。esbuild には define オプションがあるのでこれを使って環境毎の値をコードへ埋め込みます。webpack を使用している場合は DefinePlugin など、それぞれに対応したものがあるはずです。

// $ esbuild index.js --define:API_BASE_URL=\"http://dev.example.com\"
function baseURL() {
  return "http://dev.example.com";
}
fetch(`${baseURL()}/api`);

環境によって define に渡す値を変更してビルドすることで、最終的なコードには開発環境の情報が残らなくなりました。

3. ビルドフェーズで不要になるコードを確定して削除する

これは、2. の define を利用して意図的にデッドコードを生みだすことで、Minifier や Compressor でそのコードを削除する方法です。この方法はデバッグ機能そのものをコードから削除できます。

説明を簡単にするため、次のコードを例にしていきます。

function test() {
    if (APP_ENV === 'production') {
        return 'production'
    }
    else {
        return 'development'
    }
}
console.log(test())

APP_ENV はビルド時に決定される値で、それが置き換えられます。production を設定してビルドした場合 esbuild ではこのようになります。

// $ yarn esbuild index.js --define:APP_ENV=\"production\"
function test() {
  if (true) {
    return "production";
  } else {
    return "development";
  }
}
console.log(test());

APP_ENV"production" === "production" となり、true となることが確実なのでそれに置き換わり else 側が完全なデッドコードとなりました。これに --minify オプションを追加すると、デッドコードが削除されます。

// $ yarn esbuild index.js --define:APP_ENV=\"production\" --minify
function test(){return"production"}console.log(test());

逆に APP_ENV が production 以外なら else 側だけが残ります。

// $ yarn esbuild index.js --define:APP_ENV=\"development\" --minify
function test(){return"development"}console.log(test());

このようにしておくと else 側には安全に開発環境向けのデバッグ機能を記述できます。この方法は terser の README.md でも紹介されています

まとめ

情報を守るためのコードの記述方法や、ビルドツールの機能について紹介してきました。ここには書いていませんが、例えば CI で特定の URL を記述を検出するなども有効な手段の 1 つでしょう。フロントエンドのコードは「すべて公開状態」なことを常に忘れずコードを記述することを心がけましょう。