こんにちは!フロントエンドからNode.jsの深層まで、JavaScriptのコードがV8エンジン上でどう息づいているかを見つめ続けているチーフアーキテクトです。
今回は、JavaScriptの基本中の基本でありながら、一歩間違えるとアプリケーション全体を致命的なセキュリティホールへと導いてしまう「グローバル変数とプロトタイプ汚染の危険な関係」についてお話ししていきますね。
「変数なんて `let` や `const` で適当に宣言しておけば動くでしょ?」なんて思っていませんか?
ここをクリアすれば、JavaScriptのランタイムの振る舞いとセキュリティの本質がグッと見えてきます。一緒にバッチリマスターしていきましょう!
—
1. なぜグローバル変数は「諸悪の根源」と呼ばれるのか
JavaScriptを書き始めた頃は、どこからでもアクセスできるグローバル変数って便利に感じられますよね。関数をまたいでデータを共有できるし、スコープの面倒なエラーに悩まされないからです。
しかし、V8エンジンやブラウザのランタイムの視点から見ると、グローバル変数(ブラウザなら `window` オブジェクトのプロパティ、Node.jsなら `global` のプロパティ)は、「アプリケーション全体に鍵をかけずに放置された共有金庫」のようなものです。
スコープチェーンとグローバル汚染のメカニズム
JavaScriptは変数を参照するとき、現在のローカルスコープから外側へ、外側へと「スコープチェーン」を辿って探します。そして、最終的に見つからない場合、それがグローバルオブジェクトのプロパティとして暗黙的に生成・参照されます。
これが、意図しない変数名の重複や、外部からの書き換え(ハイジャック)の隙を生む原因になります。
—
2. プロトタイプ汚染(Prototype Pollution)とは何か?
さて、ここからが本題です。グローバル変数や不特定多数の入力(ユーザーからのJSONなど)が、なぜ「プロトタイプ汚染」という恐ろしい脆弱性に繋がるのでしょうか。
JavaScriptはクラスベースではなく、プロトタイプベースの言語です。すべてのオブジェクトは、親である `Object.prototype` からメソッドやプロパティを継承(委譲)しています。
もし、攻撃者がグローバルなスコープや脆弱なオブジェクトの結合(マージ)処理を利用して、この根本である `Object.prototype` を書き換えることができたらどうなるでしょう?
アプリケーション内のすべてのオブジェクトが、意図しないプロパティや悪意あるコードを継承してしまうことになります。これが「プロトタイプ汚染」です。
危険なコードの具体例
例えば、再帰的にオブジェクトをマージする(深層コピーや設定の結合を行う)次のようなよくあるユーティリティ関数を考えてみましょう。
/
- 脆弱なオブジェクト結合関数(絶対に真似しないでください!)
/
function unsafeMerge(target, source) {
for (let key in source) {
if (typeof source[key] === ‘object’ && source[key] !== null) {
if (!target[key]) target[key] = {};
unsafeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// 攻撃者が外部から悪意あるJSON(ペイロード)を送り込んだとします
const maliciousPayload = JSON.parse(‘{“__proto__”: {“isAdmin”: true}}’);
const userConfig = {};
// 結合を実行
unsafeMerge(userConfig, maliciousPayload);
// — ここからが恐怖の瞬間です —
const innocentObject = {};
console.log(innocentObject.isAdmin);
// なぜか `true` が出力されてしまう!
// 自分自身には定義していないのに、Object.prototypeが汚染されたため、
// アプリケーション内のすべての空オブジェクトがisAdminプロパティを持ってしまいました。
このコードでは、`__proto__` キーをそのままイテレートして代入してしまったため、JavaScriptの根幹であるプロトタイプチェーンの頂点が書き換わってしまいました。もしサーバーサイド(Node.js)でこのような汚染が起きれば、認証チェック(`user.isAdmin`)がすべて突破され、システムが乗っ取られることに繋がります。
—
3. なぜグローバル変数の管理がこれを加速させるのか
「でも、うちはちゃんとJSONをパースしているし大丈夫だよ」と思いましたか?
ここでグローバル変数の話に戻ります。
もし、アプリケーションのあちこちで `var` を使ったり、意図せずグローバルスコープに設定オブジェクトを平置きにしていたりすると、プログラムのどこからでもそのオブジェクトにアクセス・上書きができる状態になります。
攻撃者は、サードパーティ製の脆弱なnpmパッケージや、XSS(クロスサイトスクリプティング)の脆弱性を突いてスクリプトを実行し、このグローバルに露出したオブジェクトを起点にしてプロトタイプ汚染を引き起こします。
- グローバルスコープの乱用 = 攻撃者がアクセスできる「足場」が広がる
- 不適切なオブジェクト操作 = プロトタイプ汚染という「武器」を手に入れる
この2つが組み合わさることで、セキュリティインシデントは一気に現実のものとなります。
—
4. 極限まで安全なコードを書くための防御策
では、このリスクからアプリケーションを守るために、私たちはどうコードを書くべきでしょうか?
チーフアーキテクトからの実践的な処方箋を授けます。
① グローバル変数を排除し、ブロックスコープ(`let` / `const`)を徹底する
変数は常に最小限のスコープ(関数スコープやブロックスコープ)に閉じ込めましょう。また、Node.jsであれば必ずモジュールシステム(CommonJSやES Modules)を活用し、変数がファイル外へ露出するのを防ぎます。
② オブジェクトのプロトタイプ汚染を防ぐテクニック
ユーザーからの入力をオブジェクトにマージ・結合する場合は、`__proto__` や `constructor`、`prototype` といった危険なキーを明示的に弾く(または無視する)ロジックを必ず組み込みます。
/
- 安全性を考慮したオブジェクトマージ関数
/
function safeMerge(target, source) {
for (let key in source) {
// プロトタイプ汚染を防ぐためのバリデーション
if (key === ‘__proto__’ || key === ‘constructor’ || key === ‘prototype’) {
continue; // 危険なキーはスキップする
}
if (typeof source[key] === ‘object’ && source[key] !== null) {
if (!target[key]) target[key] = {};
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
③ オブジェクトを完全に凍結する(`Object.freeze`)
変更されるべきではない設定オブジェクトや定数は、あらかじめ `Object.freeze()` で保護し、後からプロトタイプを含めて改ざんできないようにするのも非常に有効なV8レイヤーでの防御策です。
const appConfig = Object.freeze({
env: ‘production’,
apiEndpoint: ‘https://api.example.com’
});
// 意図しない書き換えや汚染を試みても、厳格モード(strict mode)ではエラーになり、
// 通常モードでも無視されます。
appConfig.env = ‘development’;
console.log(appConfig.env); // ‘production’ のまま
—
まとめ
今回は、変数スコープの基本であるグローバル変数と、それが引き起こすプロトタイプ汚染のメカニズム、そしてその防御策について深く解説しました。
- グローバル変数は、アプリケーション全体の「共有金庫」。管理を怠ると攻撃の足場になる。
- JavaScriptのプロトタイプ機構は強力だが、不安全なマージ処理によって全体が汚染される危険がある。
- `let` / `const` によるスコープの局所化と、危険なキー(`__proto__` 等)のフィルタリングで堅牢なコードを構築する。
JavaScriptの挙動とメモリの仕組みを正しく理解すれば、恐れるものは何もありません。ぜひ今日の開発から、より安全で美しいコードを意識してみてくださいね。あなたのコードが、世界をより安全で素晴らしいものにすることを応援しています!