Prototype Pollution #
概要 #
JavaScriptでは主に"継承"を実現するための__proto__
プロパティが存在します。
Prototype Pollutionは、この__proto__
プロパティを通じて特定のオブジェクトの内容を不正に変更する攻撃手法またはそれに対する脆弱性です。
影響 #
Prototype Pollutionの影響は、さまざまで発生箇所やアプリケーションのロジックに依存します。 最悪の場合ではサーバサイドでの任意コード実行につながることがあり、そのほかの場合ではXSSやSQLインジェクションなどの脆弱性にもつながる可能性があります。
原因 #
攻撃者が__proto__
プロパティなどを経由して、特定のオブジェクトのprototypeを操作可能であることがPrototype Pollutionの原因です。
攻撃手法 #
実際にオブジェクトの__proto__
プロパティが操作可能なケースとして、下記2つの例を紹介します。
- オブジェクトに対してmergeやcloneの操作をするケース
setValue(obj, key, value)
のようにオブジェクトのプロパティを設定するケース
1. オブジェクトに対してmergeやcloneの操作を行うケース #
以下のmerge(tgt, src)
は、引数のtgt
オブジェクトにsrc
のプロパティを再帰的にマージします。
この実装では与えられたオブジェクトのkey
の値をチェックしておらず、任意のkey
に対して任意の値を代入できます。(10行目)
function isObject(obj) {
return obj !== null && typeof obj === 'object';
}
function merge(tgt, src) {
for (let key in src) {
if (isObject(tgt[key]) && isObject(src[key])) {
merge(tgt[key], src[key]);
} else {
tgt[key] = src[key];
}
}
return tgt;
}
merge(
{a: 1, b: 2},
JSON.parse('{"__proto__": {"polluted": 1}}')
);
const obj = {};
console.log(obj.polluted); // => 1
上記のPoCでは、引数src
に{"__proto__": {"polluted": 1}}
というオブジェクトを渡しています。その値を10行目でtgt[__proto__] = {"polluted":1}
のような代入が実行されることで攻撃が成功しています。
2. setValue(obj, key, value)
のようにオブジェクトのプロパティを設定するケース
#
以下のsetValue(obj, key, value)
は、引数のobj
オブジェクトに{key:value}
のプロパティを追加します。また、key
にチェーン演算子を用いて指定することで深くに位置するプロパティを追加します。
この実装でも引数key
の値をチェックしておらず、任意のkey
に対して任意の値を代入できます。(13行目)
function isObject(obj) {
return obj !== null && typeof obj === 'object';
}
function setValue(obj, key, value) {
const keylist = key.split('.');
const e = keylist.shift();
if (keylist.length > 0) {
if (!isObject(obj[e])) obj[e] = {};
setValue(obj[e], keylist.join('.'), value);
} else {
obj[key] = value;
return obj;
}}
setValue({}, "__proto__.polluted", 1);
const a = "";
console.log(a.polluted); // => 1
上記のPoCでは、引数key
に__proto__.polluted
という値を渡しています。その値が再帰的に処理されることで結果的に13行目でobj[__proto__][polluted] = 1
のような代入が実行されることで攻撃が成功しています。
事例紹介 #
- NVD - cve-2019-7609
- #1106238 Stored XSS via Mermaid Prototype Pollution vulnerability
- #454365 Prototype pollution attack through jQuery $.extend
対策 #
Prototype Pollutionの対策にはいくつか方法があるため、アプリケーションの規模や副作用を考慮して、適切な対策を選択するようにしてください。 ここでは代表的な下記5つの方法を紹介します。
- ライブラリを利用する方法
Object.freeze
を使用する方法Object
の代わりにMap
を使う方法- オブジェクトの作成を
Object.create(null)
で行う方法 - Schema validation of JSON input
ライブラリを利用する方法 #
要件を満たす場合、Prototype Pollutionの影響を受けずにオブジェクトに対してmergeやcloneを行うライブラリを利用することで防ぐことができます。
Object.freeze
を使用する方法
#
Object.freeze()を使用して、ObjectやObject.prototypeを変更できないようにすることで、prototypeが意図せず汚染されないようにできます。
下記のコードは、実際にObject.freeze()
を使用して実際に対策できることを示すPoCです。
Object.freeze(Object.prototype);
Object.freeze(Object);
({}.__proto__.test = 123);
console.log({}.test); // => undefined
なお、注意点として、(上記のPoCを実際に動かしてみてもわかる通り、)Object.freeze()
によってfreezeされたオブジェクトの変更は、特にエラーや例外が起こることなく失敗します。
そのため、本対策を適用し意図しない副作用が発生した場合でも、その発見が困難な場合もあります。
したがって、この対策は(依存ライブラリを含めた)アプリケーションの現状の実装を考慮して、慎重に適用することを推奨します。
Object
の代わりにMap
を使う方法
#
ES6以降では、Object
の代わりにMap
が利用できます。
Object
で単純にkey/valueのデータ構造を利用している場合は、それらをMap
に置き換えることで、Prototype Pollutionを防ぐことができます。
オブジェクトの作成をObject.create(null)
で行う方法
#
Object.create()
にnull
を渡し、prototypeを引き継がないオブジェクトを作成することで、__proto__
経由でprototypeが汚染されることを防ぐことができます。
下記のようなコードで、実際に__proto__
経由でprototypeにアクセスできないことを実際に確認できます。
var obj = Object.create(null);
console.log(obj.__proto__ === Object.prototype); // => false
console.log(obj.__proto__); // => undefined
もし{"a": 1, "b": 2}
のようなもともといくつかのプロパティを持つオブジェクトを作成する場合は、下記のようにObject.assign()
を併せて利用する必要があります。
var obj = Object.assign(Object.create(null), { a: 1, b: 2 });
なお、このように作成されたオブジェクトのprototypeはObject.prototype
の参照ではありません。そのため、hasOwnProperty()
のようなObject.prototype
のメソッドはプロトタイプチェーン経由で呼び出せません。
もし、そのようなメソッドを呼び出したい場合はObject.prototype.hasOwnProperty
のように明記する必要があります。
var obj = Object.create(null);
obj.a = 1;
Object.prototype.hasOwnProperty.call(obj, "a"); // => true
Object.create(null)
による本対策を適用する場合は、このような副作用に留意してください。
Schema validation of JSON input #
JSON SchemaではadditionalProperties:false
を指定することで、想定していないプロパティを禁止できます。
適切なJSON Schemeを用いてバリデーションを行うことでPrototype Pollutionの対策ができます。
以下は、前述の関数setValue()
に対し、ajvを用いて対策する例です。
const schema = {
type: "object",
properties: {
// 想定しているプロパティ
},
additionalProperties: false,
};
const validate = ajv.compile(schema);
function setValue(obj, key, value) {
const keylist = key.split(".");
const e = keylist.shift();
if (keylist.length > 0) {
if (!isObject(obj[e])) obj[e] = {};
setValue(obj[e], keylist.join("."), value);
} else {
obj[key] = value;
if (!validate(obj)) {
// handling
throw "Invalid Obj";
}
return obj;
}
}
setValue({}, "__proto__.polluted", 1); // => Exception raised
ただし、この場合、properties
に含んでいないプロパティは一切追加ができなくなることに注意が必要です。
任意のプロパティを受け入れつつ対策をする場合、単に追加されるkeyに悪意のある値が指定されないように制限する対策も有効です。
function setValue(obj, key, value) {
const keylist = key.split(".");
const e = keylist.shift();
if (keylist.length > 0) {
if (!isObject(obj[e])) obj[e] = {};
if (e !== "__proto__" && e !== "constructor") {
setValue(obj[e], keylist.join("."), value);
}
} else {
obj[key] = value;
return obj;
}
}
setValue({}, "__proto__.polluted", 1);
const a = "";
console.log(a.polluted); // => undefined
診断方法 #
基本的な診断方法 #
これまでに説明したPrototype Pollutionの基本原理や攻撃手法などを踏まえ、任意のオブジェクトのprototypeが不正に変更できないかを検証してください。
DOM Invaderを用いた効率的な診断 #
DOM InvaderはBurpSuiteの機能でDOM XSSのテストやpostMessage()
の操作を用いたテストの支援を提供します。この機能を用いることでクライアントサイドのPrototype Pollutionの自動検出や手動での検証の補助として利用可能です。
詳しい検証方法はTesting for client-side prototype pollutionで丁寧に解説されているので、こちらを参照してください。
学習方法/参考文献 #
- HoLyVieR/prototype-pollution-nsec18: Content released at NorthSec 2018 for my talk on prototype pollution
- Olivier Arteau – Prototype pollution attacks in NodeJS applications - YouTube
- Node.js における prototype 汚染攻撃への対策 - SST エンジニアブログ
- 【1 分見て】実例から学ぶ prototype pollution【kurenaif 勉強日記】 - YouTube
- Prototype pollution: The dangerous and underrated vulnerability impacting JavaScript applications | The Daily Swig
- BlackFan/client-side-prototype-pollution: Prototype Pollution and useful Script Gadgets
- Object.prototype.proto - JavaScript | MDN