【入門編】プロトタイプ汚染と変数スコープ:グローバル変数が引き起こすセキュリティリスク – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは!フロントエンドからNode.jsの深層まで、日夜JavaScriptのコードと向き合っているシニアアーキテクトです。

今回は、JavaScript初学者や他言語からやってきた開発者が最もハマりやすく、かつ実務の現場では「致命的なセキュリティインシデント」へと直結するプロトタイプ汚染と変数スコープの闇についてお話しします。

「たかが変数の宣言ミスでしょう?」なんて思っていませんか?
実はその油断が、アプリケーション全体の乗っ取りを許す隙を生むんです。ここをしっかりとクリアすれば、あなたのJavaScriptの理解度はワンランク上のステージに上がりますよ。一緒に本質をマスターしていきましょう!

—

1. なぜ「うっかりグローバル変数」が生まれるのか?

JavaScriptには、変数を宣言するためのキーワードとして `let` や `const` が用意されていますよね。しかし、歴史的な経緯や文法的な緩さ(あるいは `var` の名残や宣言のし忘れ)から、意図せず変数がグローバルスコープ(どこからでもアクセスできる空間)に漏れ出してしまうことがあります。

まずは、よくある「やってはいけない」コードを見てみましょう。

// 【危険なコード例】スコープを意識していない関数
function updateUserProfile(userId, newName) {
// 宣言キーワード(let / const / var)を書き忘れた!
// これにより、userNameは自動的に「グローバル変数」として扱われます。
userName = newName;

console.log(`ユーザーID: ${userId} の名前を ${userName} に更新しました。`);
}

updateUserProfile(42, “悪意あるユーザー”);

// 関数を抜けた後も、グローバルオブジェクト(ブラウザならwindow)に突き刺さっている
console.log(window.userName); // 実行環境によっては “悪意あるユーザー” が出力されてしまう!

なぜこれが危険なのか?

JavaScriptのランタイム(V8エンジンなど)において、グローバルオブジェクトはアプリケーション内のすべてのスクリプト、そして読み込んでいるサードパーティ製のライブラリ(npmパッケージなど)から共有される共通の「広場」のようなものです。

ここに無防備に名前空間を汚染する変数を放り込むと、他のコードが意図せぬ上書きをされたり、予期せぬバグの温床になったりします。そして、これがさらに恐ろしい「プロトタイプ汚染」の引き金になるのです。

—

2. プロトタイプ汚染(Prototype Pollution)のメカニズム

JavaScriptは、クラスベースではなくプロトタイプベースのオブジェクト指向言語です。すべてのオブジェクトは、親である `Object.prototype` からプロパティやメソッドを受け継いで(継承して)います。

ここで、先ほどの「グローバル変数の漏洩」や「不安全なオブジェクトの動的操作」が組み合わさると、何が起きるでしょうか。図解的にイメージしてみましょう。

[ 悪意ある入力データ ]
│
▼
{ “__proto__”: { “isAdmin”: true } } <-- プロトタイプを書き換えるキー! │ ▼ [ 脆弱なマージ関数 ] ---> すべてのオブジェクトの親 (Object.prototype) を汚染!
│
▼
[ アプリケーション全体 ]
-> ログインしていない一般ユーザーのオブジェクトまで “isAdmin: true” を継承してしまう!

実際のコードで、この恐ろしい現象がどのように引き起こされるのかを見てみます。

// 【実例】再帰的にオブジェクトをマージ(結合)する脆弱な関数
function unsafeDeepMerge(target, source) {
for (let key in source) {
if (typeof source[key] === ‘object’ && source[key] !== null) {
if (!target[key]) target[key] = {};
unsafeDeepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}

// 攻撃者が送り込んできた不正なJSONデータ(ペイロード)
const maliciousPayload = JSON.parse(‘{“__proto__”: {“isAdmin”: true}}’);

// 空のオブジェクトに対してマージを実行してしまう
const userSettings = {};
unsafeDeepMerge(userSettings, maliciousPayload);

// — ここからが恐怖の瞬間 —

// 全く関係ない、新しく作られた別のユーザーオブジェクトを確認してみる
const anotherUser = {};

// 驚くべきことに、自分で設定していないはずの “isAdmin” が true になっている!
console.log(anotherUser.isAdmin); // true !!!

なぜこんなことが起きるのか?

JavaScriptのオブジェクトが持つ `__proto__` プロパティ経由で、すべてのオブジェクトの根源である `Object.prototype` に `isAdmin: true` が書き込まれてしまいました。結果として、アプリケーション内で新しく生成されるすべての空オブジェクトが、その毒(汚染されたプロパティ)を自動的に継承してしまったのです。

もしこれが権限管理システムであれば、一般ユーザーが特権管理者権限をいとも簡単に奪取できてしまいますよね。これがプロトタイプ汚染の脅威です。

—

3. 変数スコープの徹底管理と防衛策

では、こうした脆弱性からアプリケーションを守るためには、どうすればよいのでしょうか。答えは非常にシンプル、かつモダンなJavaScriptの基本を徹底することにあります。

対策1: 厳格モード(`use strict`)の常態化

モジュールシステム(ES Modules)やNode.js環境ではデフォルトで有効になっていますが、スクリプトの先頭に `”use strict”;` を宣言することで、うっかり宣言し忘れた変数への代入を即座にエラー(ReferenceError)として検知させることができます。

“use strict”;

function secureFunction(userId, newName) {
// 宣言キーワードなしで代入しようとすると、ここで即座にプログラムがクラッシュ(安全な失敗)する
// ReferenceError: userName is not defined
userName = newName;
}

「エラーで止まってくれる」というのは、開発時において最高値のセキュリティーツールです。バグを隠蔽するよりも、即座に教えてもらう方が遥かに安全だからです。

対策2: `let` と `const` によるブロックスコープの活用

古いコードで使われていた `var` は、関数スコープを持ち、巻き上げ(Hoisting)によって予期せぬ場所で参照できてしまう問題があります。常に `const`(再代入不可)をファーストチョイスとし、値が変わる変数にのみ `let` を使いましょう。変数の生存期間(ライフサイクル)を最小限に絞ることで、スコープの漏洩を防げます。

対策3: プロトタイプ汚染を防ぐ防御的コーディング

オブジェクトのマージやクローンを行う際は、ユーザーからの入力をそのまま信用してキーに代入してはいけません。

function safeDeepMerge(target, source) {
for (let key in source) {
// __proto__ や constructor, prototype などの危険なキーの混入を徹底的にガードする
if (key === ‘__proto__’ || key === ‘constructor’ || key === ‘prototype’) {
continue; // スキップする
}

if (typeof source[key] === ‘object’ && source[key] !== null) {
if (!target[key]) target[key] = {};
safeDeepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}

また、モダンな環境であれば、オブジェクトのプロトタイプを凍結する `Object.freeze(Object.prototype)` を用いたり、そもそも安全に設計されたサードパーティ製のユーティリティライブラリ(Lodashの最新版など、脆弱性対策済みのもの)を使用することが鉄則です。

—

まとめ

変数の宣言とスコープ、そしてプロトタイプチェーンの仕組みは、一見すると地味な基礎知識に思えるかもしれません。しかし、その裏側ではV8エンジンがメモリ空間を効率的に管理し、私たちのコードの安全性を支えています。

  • 変数は必ず `let` / `const` で適切に宣言する(`var` は使わない)。
  • グローバル変数の汚染を避け、スコープを可能な限り狭く保つ。
  • 外部からの入力データをオブジェクトに組み込む際は、`__proto__` などの汚染リスクを常に疑う。

この3つを意識するだけで、あなたの書くJavaScriptコードの安全性は劇的に跳ね上がります。
基本を固めて、セキュアで美しいコードを書いていきましょう!

タイトルとURLをコピーしました