Prototype Pollution

Prototype Pollution #

概要 #

JavaScriptでは主に"継承"を実現するための__proto__プロパティが存在します。 Prototype Pollutionは、この__proto__プロパティを通じて特定のオブジェクトの内容を不正に変更する攻撃手法またはそれに対する脆弱性です。

影響 #

Prototype Pollutionの影響は、さまざまで発生箇所やアプリケーションのロジックに依存します。 最悪の場合ではサーバサイドでの任意コード実行につながることがあり、そのほかの場合ではXSSやSQLインジェクションなどの脆弱性にもつながる可能性があります。

原因 #

攻撃者が__proto__プロパティなどを経由して、特定のオブジェクトのprototypeを操作可能であることがPrototype Pollutionの原因です。

攻撃手法 #

実際にオブジェクトの__proto__プロパティが操作可能なケースとして、下記2つの例を紹介します。

  1. オブジェクトに対してmergeやcloneの操作をするケース
  2. 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のような代入が実行されることで攻撃が成功しています。

事例紹介 #

対策 #

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で丁寧に解説されているので、こちらを参照してください。

学習方法/参考文献 #