プロトタイプ汚染

プロトタイプ汚染

説明

ユーザー入力で入れ子のオブジェクトのプロパティパスを決めると、__proto__ や constructor.prototype をたどって、プロトタイプ自体に値を書き込める場合があります。共有プロトタイプが汚染されると、それを継承するほかのオブジェクトにも攻撃者のプロパティが現れる可能性があります。

想定される影響

  • 汚染されたプロパティを読むアプリケーションの処理が、想定外の動作をするおそれがあります。
  • そのプロパティを認証や認可の判断に使っていると、検証を回避される場合があります。
  • 関数や設定値の変更により、障害やデータ破損につながる可能性があります。

対処方法

  • パスの各要素が文字列であることを確認し、__proto__、constructor、prototype を拒否してください。
  • サーバー側の許可リストで変更可能なフィールドを限定し、入れ子をたどる際は継承されたプロパティに進まないでください。
  • 辞書として使う保存先には、Object.create(null) や Map を検討してください。
  • Lodashやdot-propなどのライブラリを使う場合は、セキュリティパッチを適用してください。ライブラリの使用は入力検証の代わりにはなりません。

例

変更前

javascript
// 変更前: 動的なプロパティパスを制限していません。
function setDeepValue(obj, keys, value) {
  keys = Array.isArray(keys) ? keys : keys.split('.')
  let cur = obj
  while (keys.length > 1) {
    const key = keys.shift()
    // 変更前: keyが'__proto__'でも進みます。
    if (!cur[key]) cur[key] = {}
    cur = cur[key]
  }
  cur[keys[0]] = value
}
// setDeepValue(user, ['__proto__', 'isAdmin'], true);

変更後

javascript
// 変更後: パス要素を検証し、自身のプロパティだけを使います。
function setDeepValueSafe(obj, keys, value) {
  const blockedKeys = ['__proto__', 'constructor', 'prototype']
  keys = Array.isArray(keys) ? [...keys] : keys.split('.')
  if (keys.length === 0 || keys.some(key =>
      typeof key !== 'string' || blockedKeys.includes(key))) {
    throw new Error('위험한 속성명입니다.')
  }
  let cur = obj
  while (keys.length > 1) {
    const key = keys.shift()
    if (!Object.hasOwn(cur, key)) cur[key] = Object.create(null)
    cur = cur[key]
    if (cur === null || typeof cur !== 'object') {
      throw new Error('위험한 속성명입니다.')
    }
  }
  cur[keys[0]] = value
}

説明:

  • 変更前: 制限なくパスをたどるため、__proto__ などを通じて共有プロトタイプに書き込める場合があります。
  • 変更後: 先に各要素の型と危険なキーを検証し、途中ではオブジェクト自身のプロパティだけをたどります。新しい保存先はプロトタイプなしで作成します。

参考資料