黒い基板上の抵抗器とコンデンサの接写
技術解説

console.logがXSSの穴に?ロガー導入時に見落とす落とし穴

目次を見る

ブラウザ上で動くデバッグ用ロガーを自作、あるいは npm パッケージから導入しているエンジニアに向けた話です。特に開発中のログ表示用 UI を本番ビルドに残している、または残す可能性がある構成のプロジェクトが対象になります。

JavaScript のロギングライブラリ Candy Logger の v2.1.0 では「Correctness Release(正確性リリース)」と銘打ち、循環参照オブジェクトの安全なシリアライズ化(オブジェクトを文字列やJSON形式に変換する処理)、Error オブジェクトの詳細な情報表示、そして XSS(クロスサイトスクリプティング、悪意あるコードをページ内に注入する攻撃)対策が同時に修正されています。この3点がセットで直っているのは偶然ではありません。ログ表示用の UI を作る以上、避けて通れない構造的な落とし穴だからです。

何が起きるか

ログビューアの多くは、渡された値をそのまま画面に描画します。

オブジェクトのキー名やタグのラベル、ログレベルの文字列、カスタムアクションのラベルなど、開発者が「自分で書いた文字列だから安全」と思い込みがちな箇所が、実はそのまま HTML として解釈されてしまうケースがあります。

たとえば次のようなオブジェクトをログに流したとします。

console.log({ \"<img src=x onerror=alert(1)>\": \"test\" });

オブジェクトのキーがそのまま DOM に挿入される実装だと、onerror ハンドラが実行されてしまいます。ユーザー入力や外部 API のレスポンスをそのままログに流している場合、攻撃者が用意した文字列がキー名として紛れ込むだけで、ログビューアの画面上でスクリプトが実行される可能性があります。

もう一つの落とし穴は、循環参照オブジェクトによるクラッシュです。

const circularObject = {};
circularObject.self = circularObject;
console.log(circularObject);

この手のオブジェクトは JSON.stringify にそのまま渡すと TypeError が発生します。ロガー内部の処理でこれが握りつぶされずにアプリケーション側に漏れると、デバッグ用のログ出力が原因で本体機能が止まるという、本末転倒な事故につながります。

なぜ起きるか

原因は3段階に分解できます。

1段目は、ログ用 UI が「表示すること」を優先して作られ、「表示する値の出所」を疑っていないことです。console.log に渡される値は、開発者のデバッグコードだけでなく、フォーム入力やAPIレスポンス、Cookie の値など外部由来のデータが混ざります。それを信頼済みの文字列として扱うと、表示層でのエスケープ処理(HTMLとして解釈されないよう記号を変換する処理)が漏れます。

2段目は、UI の実装がインラインイベントハンドラ(onclick=\"...\" のようにHTML属性に直接書くJavaScript)やグローバル変数(window.__candy* のような形でページ全体からアクセスできる変数)に依存していることです。これは実装を手早く進めるための近道になりがちですが、厳格な CSP(Content Security Policy、ページで実行できるスクリプトの種類を制限するブラウザの仕組み)環境では動作しないだけでなく、攻撃対象領域(アタックサーフェス)を広げる要因にもなります。

3段目は、React の StrictMode(開発時に副作用を意図的に二重実行して不具合を検出する仕組み)や HMR(Hot Module Replacement、コードを保存すると画面をリロードせずに差分だけ反映する開発機能)によって、コンソールの上書き処理(overrideConsole() のような初期化関数)が複数回呼ばれることです。冪等性(同じ処理を何度実行しても結果が変わらない性質)を考慮していない実装では、二重登録によるイベントリスナーの重複やメモリリークにつながります。

自分のプロジェクトが該当するか確認する

まず、使っているロガーの実装が DOM にどう値を書き込んでいるかを確認します。

# プロジェクト内でinnerHTML経由の描画がないか確認
grep -rn \"innerHTML\" node_modules/自分の使っているロガー名/dist

innerHTML に外部由来の文字列を直接連結している箇所があれば要注意です。エスケープ処理を通さずにキー名やラベルをそのまま埋め込んでいる可能性があります。

次に、package.json でロガーのバージョンを確認します。

npm ls | grep logger
cat node_modules/自分の使っているロガー名/package.json | grep version

自作のログビューアを使っている場合は、CSP を一時的に厳格化してみるのが手早い確認方法です。ブラウザの開発者ツールでレスポンスヘッダーに次のような CSP を追加してみます。

Content-Security-Policy: script-src 'self'; style-src 'self'

インラインの onclick やグローバル変数に依存した実装だと、この時点でログビューアのボタンが反応しなくなります。これは CSP 非対応の兆候です。

最後に、循環参照を含むオブジェクトを実際にログに流してみます。

const obj = {};
obj.self = obj;
console.log(obj);

ここでコンソールにエラーが表示されたり、アプリケーションの他の処理が止まったりする場合、シリアライズ処理に問題があります。

対策の手順

  • ログビューアが値を描画する箇所を洗い出し、innerHTML ではなく textContent や仮想 DOM の子要素として値を渡す実装に置き換える
  • カスタムのシリアライザーを使っている場合、Map・Set・Date・RegExp・BigInt・Symbol・DOM ノード・循環参照オブジェクトそれぞれについて、変換結果を実際に確認する
  • 本番ビルドに CSP ヘッダーを設定し、その状態でログビューア一式が動作するかを開発環境で先に検証する
  • コンソールの上書き処理を行う初期化関数がある場合、二重初期化を防ぐガード(すでに上書き済みかを判定するフラグなど)が入っているかコードを読んで確認する
  • 既存の外部ライブラリを使っている場合は、changelog やリリースノートで「XSS」「CSP」「serialization」といったキーワードでの修正履歴を確認し、該当する脆弱性が過去に報告されていないかをチェックする

外部パッケージを使っている場合は、これらを自分で作り込む必要はありません。バージョンを最新に上げ、上記の確認手順で実際に自分の環境で再現しないかを試すだけで十分です。

まとめ

ログビューアは「開発を助けるための道具」であるがゆえに、セキュリティチェックの対象から外れがちです。

しかし外部由来のデータをそのまま画面に描画する以上、XSS のリスクは通常の UI コンポーネントと変わりません。

まず手元の環境で、循環参照オブジェクトのログ出力とCSPを厳格化した状態での動作確認を試してみてください。

その2点が通れば、少なくとも今回取り上げたような落とし穴には該当しない可能性が高いと判断できます。

参考

🍬 Candy Logger v2.1.0 - The Correctness Release

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。