コードレビュー:そのグローバル変数、本当に「安全」と言い切れますか?
レガシーなスクリプト、あるいは焦燥感に駆られた開発の過程で、ついやってしまいがちなのが「とりあえずグローバルスコープに置く」という実装だ。
「関数間でデータを共有したい」「設定値をどこからでも参照できるようにしたい」――その魔力に屈した瞬間、あなたのコードベースはV8エンジンのヒープメモリ上において、予期せぬ脆弱性の温床へと変貌する。
今日のコードレビューで私が最も強く指摘したいのは、「グローバル変数の乱用が、いかにしてプロトタイプ汚染(Prototype Pollution)の踏み台となり、アプリケーションを致命的なセキュリティリスクに晒すか」という点だ。
一般的な入門書は「グローバル変数は名前空間を汚染するので控えましょう」と優しく諭すが、プロの現場ではそんな生ぬるい理由では動かない。V8の内部構造、スコープチェーンの解決メカニズム、そしてオブジェクトのプロトタイプチェーンが交差したとき、何が起きるのか。実務の防衛的プログラミングの観点から、そのメカニズムと完全防衛の設計パターンを徹底的に叩き込む。
—
1. スコープチェーンとグローバル汚染のメカニズム
JavaScript(V8ランタイム)において、変数を宣言なしで代用したり、`window`(ブラウザ)や`global`(Node.js)オブジェクトのプロパティとして値を直接代号したりする行為は、グローバルスコープのヒープ領域を直接書き換える行為に他ならない。
スコープチェーンの探索において、V8はまずローカルスコープ(Lexical Environment)を確認し、見つからなければ外側、最終的にグローバルオブジェクトへと遡る。このグローバルオブジェクトのプロパティは、アプリケーション全体から無防備にアクセス・改変が可能だ。
ここで問題になるのが、JavaScriptのダイナミックなオブジェクト拡張性である。次のようなコードを見たとき、あなたは何を感じるだろうか?
// ❌ 危険なアンチパターン:グローバル空間に設定オブジェクトを露出させる
var appConfig = {
theme: ‘light’,
isAdmin: false
};
function updateSettings(userinput) {
// 外部からの入力をそのままマージしてしまう脆弱な処理
for (let key in userinput) {
appConfig[key] = userinput[key];
}
}
一見、何の問題もないオブジェクトのマージ処理に見えるかもしれない。しかし、もしこの `userinput` が悪意ある攻撃者によって制御されており、次のようなペイロードを含んでいたとしたらどうなるか?
const maliciousInput = JSON.parse(‘{“__proto__”: {“isAdmin”: true}}’);
updateSettings(maliciousInput);
この瞬間、`appConfig` だけではなく、アプリケーション内で生成されるすべてのプレーンオブジェクトのデフォルト値が汚染される。 すべてのオブジェクトのプロトタイプである `Object.prototype` に `isAdmin: true` がインジェクションされてしまうからだ。これがプロトタイプ汚染の悪夢である。
—
2. プロトタイプ汚染が引き起こすセキュリティ・カオス
プロトタイプ汚染が厄介なのは、エラーを吐かずに「静かに挙動がバグる」点にある。DOM操作や非同期APIのルーティング、権限管理の判定処理において、以下のようなコードがあったとしよう。
// 権限チェックのつもり
function renderAdminDashboard(user) {
// ユーザー自身が isAdmin プロパティを持っていなくても、
// プロトタイプが汚染されていれば user.isAdmin は true を返す!
if (user.isAdmin) {
console.log(‘機密データをレンダリングします…’);
}
}
グローバルに存在するオブジェクトや、ディープマージ(Deep Merge)ライブラリの不備を突いたプロトタイプ汚染は、認証バイパス、意図しないコード実行(RCE)、あるいはDoS攻撃へと直結する。フロントエンドのコンポーネント設計においても、状態管理ストア(ReduxやZustandなど)の初期化時に外部APIのレスポンスを不十分にマージすると、この毒がアプリケーション全体に回り始める。
—
3. 実務で即座に使える「堅牢な防衛設計パターン」
では、私たちはこの脅威からコードベースをどう守るべきか。答えは明確だ。「グローバル変数を排除し、スコープを極限まで閉じ込め、入力を完全にサニタイズする」こと。
以下に、プロダクションコードで即座に採用すべき、モジュール化された安全な設計パターンを提示する。
パターンA: クロージャとIIFE(あるいはESモジュール)によるスコープの完全カプセル化
変数をグローバルに露出させず、モジュールスコープ(または関数スコープ)内に閉じ込めることで、外部からの不正な介入経路を物理的に断つ。
/
- @file secure-config.js
- ESモジュールを用いた安全な設定管理カプセル化の例
/
// 密閉されたモジュールスコープ(グローバルを汚染しない)
const _internalState = Object.freeze({
theme: ‘light’,
version: ‘1.0.0’
});
// 危険なキーを完全に排除した安全なマージ関数
function secureDeepFreezeAndMerge(target, source) {
// Object.prototype への汚染を防ぐためのブラックリスト防御
const FORBIDDEN_KEYS = [‘__proto__’, ‘constructor’, ‘prototype’];
for (const key of Object.keys(source)) {
if (FORBIDDEN_KEYS.includes(key)) {
console.warn(`セキュリティ警告: 不正なプロパティ “${key}” のインジェクションを検知し、ブロックしました。`);
continue;
}
// 再帰的マージを行う場合のプロトタイプ汚染チェック
if (source[key] && typeof source[key] === ‘object’) {
if (!target[key]) target[key] = {};
secureDeepFreezeAndMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// 外部へ公開するAPIを限定(カプセル化)
export const ConfigManager = {
get(key) {
return _internalState[key];
},
// 外部入力を受け付ける際は、プロトタイプ汚染対策を施した専用関数を通す
update(userInput) {
// オブジェクトのプロトタイプを完全に持たない(Null原型)オブジェクトの生成
const safePayload = Object.create(null);
Object.assign(safePayload, userInput);
// イミュータブルに安全性を担保して処理
return secureDeepFreezeAndMerge({}, safePayload);
}
};
パターンB: `Object.create(null)` による「辞書オブジェクト」の徹底
JavaScriptでハッシュマップ(連想配列)としてオブジェクトを使う場合、プレーンな `{}` を使うと必ず `Object.prototype` を継承してしまう。これを防ぐためには、プロトタイプを持たない純粋なハッシュマップを作成する必要がある。
/
- プロトタイプチェーンを持たない純粋な辞書の生成
/
function createSafeDictionary() {
// 原型(prototype)が null のオブジェクトを作成
// これにより __proto__ プロダクトは存在せず、プロトタイプ汚染は構造的に不可能になる
return Object.create(null);
}
// 実行例
const userCache = createSafeDictionary();
userCache[‘admin’] = { permissions: [‘read’, ‘write’] };
// 万が一、攻撃者が入力を捏造しても Object.prototype には影響しない
console.log(userCache.__proto__); // undefined
—
チーフアーキテクトからの最終提言
変数のスコープ管理やプロトタイプ汚染への対策は、「動けばいい」という実装フェーズの後付けで何とかできるものではない。それはアーキテクチャの根幹であり、開発チーム全体の規律だ。
1. `var` は論外、`let` / `const` でブロックスコープを厳守せよ。
2. グローバルスコープに状態(State)を置くな。 データは常に最小のスコープに閉じ込め、必要最低限のインターフェース(アクセサ)を通じてのみやり取りさせろ。
3. 外部からの入力(APIレスポンス、クエリパラメータ、JSON.parse)を盲信するな。 オブジェクトをマージする際は、`__proto__` や `constructor` といった危険なキーの混入を必ずバリデーションせよ。
コードレビューの際、安易にグローバル変数を使っているコードを見つけたら、こう問いかけてほしい。「その変数、本当にグローバルでなければならないのか?」と。その問いの積み重ねこそが、セキュアで高速、かつ保守性の高いプロダクトを作り上げる唯一の道である。