実行時コード生成という「パンドラの箱」:eval()とsetTimeout()の解剖学
セキュリティの現場で長年ログを追っていると、いまだに「なぜこんなことをしたのか」と頭を抱えたくなるコードに出くわす。その筆頭が、JavaScriptにおける動的コード評価関数、すなわち eval() や setTimeout() への文字列渡しだ。
これらは単なる「便利なメソッド」ではない。アプリケーションの実行コンテキストという神聖な領域に、外部から注入された「文字列」を直接実行させるという、自己破壊的なアーキテクチャの入り口である。本稿では、この脆弱性がなぜ現代の防御網を容易にすり抜けるのか、そして我々エンジニアがどうアーキテクチャを再設計すべきかについて、泥臭い実体験を交えて紐解いていく。
—
1. なぜ「文字列渡し」がXSSの終着点となるのか
eval() が受け取るのは文字列であり、JavaScriptエンジンはそれを「コード」として解釈する。ここで致命的なのは、「データと命令の境界」が完全に消失する点だ。
例えば、ユーザー入力をテンプレートエンジンの脆弱性やDOM操作を介して setTimeout() に渡すケースを考えてみてほしい。
// 非常に危険なコード例
// ユーザーが制御可能なクエリパラメータから値を取得
const userInput = new URLSearchParams(window.location.search).get(‘callback’);
// setTimeoutの第一引数に文字列を渡すと、それはevalと同様に評価される
// 攻撃者は ?callback=alert(document.cookie) と入力するだけで、
// 任意のJSコードをブラウザの実行コンテキスト内で走らせることが可能になる
setTimeout(userInput, 1000);
これは単なるXSSではない。「実行時コード生成」という特権を利用した、プロセスの乗っ取りである。攻撃者は、サンドボックスを脱出するための複雑なペイロードを、文字列として隠蔽して送り込むことができる。
—
2. 根本原因:JavaScriptエンジンの実行パイプライン
この問題の本質は、ブラウザのJavaScriptエンジンが「パース」と「実行」を同一のスコープで行うプロセスにある。
eval() が実行されると、エンジンは現在のスコープを凍結し、渡された文字列を字句解析(Lexical Analysis)および構文解析(Parsing)のパイプラインに再投入する。もし、この文字列の中に fetch() や XMLHttpRequest を使った外部通信、あるいは既存のグローバルオブジェクトの書き換えが含まれていれば、それは正規のスクリプトと何ら区別がつかない。
特に最近の脅威として注意すべきは、生成AIを用いたコード生成との親和性だ。LLMを介した動的なUI構築において、AIが「動的コード生成」のコードを提案してしまうケースが多発している。この「AIが生成した脆弱性」をそのままCI/CDパイプラインに乗せてしまうと、防御層のガードレイルは容易に突破される。
—
3. 推奨されるアーキテクチャ:CSPによる論理的遮断
「evalを使わない」という原則は当然だが、大規模なレガシーシステムでは即座に排除できないこともあるだろう。そこで、我々アーキテクトが導入すべきは、Content Security Policy (CSP) による強制的な実行制限である。
CSP設定のベストプラクティス
unsafe-eval を許可するポリシーは、実質的にセキュリティを放棄しているに等しい。以下のように、信頼できるスクリプトのみを許可する設計へ移行せよ。
CSPヘッダーの例
‘unsafe-eval’ を含めず、nonceを用いて動的スクリプトを制御する
Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘nonce-R4nd0mSt3r’; object-src ‘none’;
もし動的な処理が必要なら、eval() を使うのではなく、JSONオブジェクトをパースしてデータとして扱うか、あるいは Web Workers を利用してメインスレッドから隔離された環境で処理を実行するアーキテクチャへの刷新を強く推奨する。
—
4. 監査の観点:静的解析から動的フックへ
テックリードとしてコードベースを監査する際、単なる文字列検索(grep eval)では不十分だ。攻撃者は、難読化された関数や window['ev'+'al']() のような動的なプロパティアクセスで目を欺く。
監査のポイント
1. AST(抽象構文木)ベースの静的解析: ESLint の no-eval ルールだけでなく、ASTを解析して「実行時にデータが混入する可能性があるDOM操作」を検知するカスタムプラグインを導入すること。
2. 実行時のフック: window.setTimeout をラップし、第一引数が文字列である場合にログを吐き出し、スタックトレースを記録する監視レイヤーを構築する。
// 開発環境でのモニタリング用ラップ
const originalSetTimeout = window.setTimeout;
window.setTimeout = function(code, delay) {
if (typeof code === ‘string’) {
console.error(“【警報】危険なeval相当の処理が検出されました:”, code);
// ここでSentry等の監視ツールへスタックトレースを送信する
}
return originalSetTimeout.apply(this, arguments);
};
—
最後に:防御側の矜持
サイバー攻撃は、常に「開発者が楽をしようとした箇所」を正確に突き刺してくる。eval() や setTimeout() への文字列渡しは、その最たる例だ。
我々が守るべきは、単なるサーバーのメモリ空間ではない。ユーザーの信頼であり、データという資産だ。コードを書くとき、常に自問してほしい。「この文字列は、私の意志を反映しているか、それとも攻撃者の踏み台になり得るか」と。
「動的な柔軟性」という誘惑を捨て、堅牢なデータ構造を設計することこそが、現代のセキュリティアーキテクトに求められる唯一の正解である。妥協なき設計を。それが、我々の仕事だ。
コメント