tl;dr
- JavaScriptでオブジェクトのdeep mergeをしたいならこの記事を読むか、あるいは最新版のlodashを使ってください
- 自力で実装したdeep mergeを使う場合は、引数にユーザーの入力(JSONなど)を使わないでください
deep merge? なにそれ
オブジェクトの引数を再帰的に設定する関数で、lodashでいえばmerge関数とかに相当。
プロトタイプ汚染の闇
JavaScriptのオブジェクトには99%プロトタイプが存在します。偉い人はオブジェクトをObject.create(null)
で作ったりしますが、そういう人は稀です。
プロトタイプの属性は、同名の属性が元のオブジェクトに存在しない場合のフォールバックとして使われます。たとえば、いつも使うhasOwnProperty
は、実はObject
のプロトタイプ内に存在するのです。
では、そのプロトタイプを書き換えられたらどうでしょうか。たとえば、hasOwnProperty
が1
になったら、({}).hasOwnProperty("foo")
といったコードがエラーを出します。もし認証系のコードで「isAdmin属性が存在すれば管理者だ」をconst isAdmin = ({}).isAdmin;
のように実装していれば、立派な権限昇格です。これは「プロトタイプ汚染」(Prototype Pollution)と呼ばれます。
そして、プロトタイプ汚染が怖いのは、deep mergeという簡単な操作で、引き起こせるからです。
Deep Merge クイックテスト
もし下のオブジェクトを空のオブジェクトにdeep mergeして、Object.prototype.isAdmin
がセットされたら、プロトタイプ汚染成功です。(自分のコードでのみ試してください... 本番環境のAPIに投げるのはお勧めしません。)
{
"__proto__": {"isAdmin": 1},
"constructor": {"prototype": {"isAdmin": 1}}
}
安全なdeep merge
一番安全なのは、lodashのようなライブラリーを使うことです。なお、最新版を使うことをお勧めします。
それでも自力で実装したい場合は
-
__proto__
、constructor
、prototype
のキーが出てきたら無視する。 - merge先の
hasOwnProperty
の結果がfalseの場合は、空のオブジェクトを作成する。(typeof merge先 === "object"
のみだと不十分。)
のような安全策を取らないといけません。荒技としてはObject.freeze(Object.prototype)
で汚染をできないようにすることもできますが、あくまで荒技です。