Skip to main content

3年の沈黙を経て、jQueryの新たなプロトタイプ汚染の脆弱性が再び出現

著者
jQuery Blog

2019年4月15日

0 分で読めます

2019年3月26日、jQueryの前回のセキュリティ脆弱性が公表されてから約3年が経った今、同じ人気のフロントエンドライブラリ jQueryに影響する新たなセキュリティ脆弱性が見つかりました。

この脆弱性はプロトタイプ汚染と呼ばれ、攻撃者がJavaScriptアプリケーションのオブジェクトプロトタイプを上書きできるものです。これにより、攻撃者が制御するプロパティがオブジェクトに注入され、JavaScript例外を発生させてサービス拒否を引き起こしたり、アプリケーションのソースコードを改ざんして攻撃者が注入したコードパスを実行させたりする可能性があります。

以下は、Node.js Security作業部会(WG)の一員としてHackerOneでトリアージした、最初の報告に含まれていた概念実証の例です。

let a = $.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}'))
console.log({}.devMode); // true

コードが示すように、jQueryの拡張APIを使って複数のオブジェクトを再帰的にマージしています。

悪用が非常に簡単な脆弱性というわけではありませんが、JavaScriptエコシステムでjQueryが広く使われているため、多数のプロジェクトやユーザーに影響する可能性があります。さらに、2018年12月にmongooseに影響したものなど、プロトタイプ汚染攻撃が実際に発生した事例もすでに確認されています。

jQueryチームはこのセキュリティ問題を修正したバージョン3.4.0をリリースしました。ぜひアップグレードしてください。

脆弱なアプリケーションを再現する

jQueryは主にフロントエンドで使われるライブラリです。クライアントサイドのアプリケーションで、プロトタイプ汚染の脆弱性がどのように現れるか見てみましょう。

攻撃はユーザー入力から始まります。これにより、悪意のある攻撃者は、開発者がサニタイズしていない、あるいは特別な処理を施す対象として認識していないオブジェクトを注入できます。

ユーザーがJSONペイロードを送信でき、それがそのまま保存されるアプリケーションを構築しているとしましょう。コンテンツの構造をユーザー自身が制御できるようにすることで、その構造に対する責任を負わずに済むよう、この機能を提供したい場合もあるでしょう。

このような場合に送信されるペイロードの例を以下に示します。

{
  “myProperty” : “a”,
  "__proto__" : { "isAdmin" : true }
}

オブジェクトを再帰的に複製する必要があるものの、方法がよくわからないとします。Googleで「JavaScriptでオブジェクトをディープクローンする」と検索すると、こちらの回答がStack Overflowの検索結果の一番上に表示されます。

4,000件以上の賛成票を集め、正解として選ばれているのを見れば、ディープコピーの例をコピー&ペーストして作業を済ませたくなるかもしれません。

var myObject = ‘{ “myProperty” : “a”, "__proto__" : { "isAdmin" : true } }’
var newObject = jQuery.extend(true, {}, JSON.parse(myObject))

Stack Overflowのアドバイスを踏まえると、このコード例では何が起きると思いますか?

  1. myObjectは、データベースのフィールドから取得した可能性のある、文字列化された形式の例です。

  2. JSON.parse()とjQueryのextend()関数を使って、myObjectのコピーを作成します。

  3. 新しく複製されたオブジェクトはnewObjectと呼ばれます。

JSON.parse()が、今回の例のように__proto__という名前のプロパティを処理する場合、そのオブジェクトの親プロパティにisAdminプロパティをtrueとして割り当てると思うかもしれません。しかし実際には、その名前のプロパティを持つオブジェクトが作成され、プロパティチェーンの機能が上書きされます。

このJSON.parse()の動作と、安全でないディープクローン機能(オブジェクトのマージとも呼ばれます)が組み合わさると、__proto__プロパティに割り当てられた値が、グローバルなJavaScriptオブジェクトに漏れ出します。

これを踏まえて、プロトタイプ汚染攻撃の仕組みを理解するため、次のコードスニペットとその影響を考えてみましょう。

var myObject = ‘{ “myProperty” : “a”, "__proto__" : { "isAdmin" : true } }’
var newObject = jQuery.extend(true, {}, JSON.parse(myObject))
// if you do console.log({}.isAdmin) you’ll get true returned
// later down the application source code we may try to detect if the user is an admin or not
If (user.isAdmin === true) {
  // do something like load the relevant interface, run an Ajax call, access localStorage, etc
}

データベースから取得したユーザーオブジェクトのisAdminプロパティに値が設定されていなければ、ユーザーオブジェクトの値は実質的に未定義です。その場合、if文でisAdminプロパティにアクセスするには、userオブジェクトのプロトタイプチェーンにある親オブジェクトを参照することになります。それはObjectであり、すでに汚染されてisAdminプロパティがtrueに設定されています。結果として、開発者の意図に反してユーザーは管理者となり、アプリケーションに大きな被害を与える可能性があります。

その他の攻撃経路を探る

先ほどの例で見たように、安全でない再帰的なマージ処理とJSON.parseの動作が組み合わさると、プロトタイプチェーンが汚染される可能性があります。

しかし、プロトタイプを変更する方法はこれだけではありません。次のコードを見てみましょう。

let myObj = {}
myObj[‘__proto__’][‘a’] = ‘a’
console.log(myObj.a)
let newObj = {}
console.log(newObj.a)

この例では、次のことを行います。

  1. 新しいmyObjを作成します。

  2. __proto__を介してプロトタイプチェーンにアクセスし、そのオブジェクトに新しい文字列プロパティを追加します。

myObjのプロトタイプは、変更したJavaScriptのObjectそのものです。そのため、これ以降に作成されるすべての新しいオブジェクトにもこのプロパティが含まれます。これはJavaScriptの仕組みによるものです。aオブジェクトのプロパティ(newObj上)に値がなければ、JavaScriptはnewObjのプロトタイプを参照して値を探します。そしてプロトタイプチェーン全体をたどり、見つかるまで再帰的に検索します。このケースではプロパティが存在するため、アクセスすると文字列値が返されます。

ここまで説明した方法で、誰かが__proto__オブジェクトへのアクセスを注入できるのか、疑問に思うかもしれません。

npm上のパッケージを一覧表示し、たとえば見やすいユーザーインターフェースに表示するアプリケーションを構築するとしましょう。

npmパッケージの一覧と、それぞれのpackage.jsonファイルの内容を送信するAPIが必要になるでしょう。

[
  {
    "cool-package": {
      "license": "MIT",
      "github": "https://github.com/someuser/cool-package"
    }
  },
  {
    "__proto__": {
      "license": "MIT",
      "github": "https://github.com/",
      “toString”: “april fools”
    }
  }
]

このAPIはnpmjs自体や同社が提供するAPIから直接届くため、あるいは攻撃者がAPIサービスを所有していない他の信頼できるデータ提供サイトから届くため、安全だと思うかもしれません。しかし、それは誤りです。お気づきのとおり、パッケージ名やpackage.jsonの内容などの詳細は、最終的にユーザーが制御できます。

このサンプルアプリケーションのシナリオをさらに進めると、開発者は配列からマップを作り、配列を繰り返し処理してデータを取り出さなくても、パッケージに簡単にアクセスできるようにしたいと考えます。

つまり、開発者は次のようにデータにアクセスしようとするかもしれません。

const pkgLicense = packageList[‘jquery’][‘license’]

このコードの動作例を以下に示します。

let list_of_npm_packages = JSON.parse(`
[
  {
    "cool-package": {
      "license": "MIT",
      "github": "https://github.com/someuser/cool-package"
    }
  },
  {
    "__proto__": {
      "toString": "april fools"
    }
  }
]
`);

list_of_npm_packages.forEach((package) => {
     for (const [propertyKey, objectValue] of Object.entries(package)) {
          for (const [name, value] of Object.entries(objectValue)) {            
            if (!packagesMap[propertyKey]) {
            packagesMap[propertyKey] = {}
            }
            packagesMap[propertyKey][name] = value 
          }
     }
})

この時点でまったく新しいオブジェクトを作成し、そのtoStringプロパティにアクセスすると、プロトタイプ汚染攻撃が明らかになります。

const a = {}
console.log(a.toString)  // prints ‘april fools’
console.log({}.toString) // prints ‘april fools’

実際に確認されたプロトタイプ汚染の脆弱性

この種の脆弱性の被害に遭ったのはjQueryだけではありません。昨年だけでも、ブラウザーとNode.jsのエコシステム全体で20件を超えるプロトタイプ汚染の脆弱性を記録しました。これらの脆弱性は、lodashのようなJavaScriptライブラリにも見つかっており、lodashの人気ゆえに多くのフロントエンドプロジェクトに影響する可能性があります。また、node.extendやdeep-extendのように、JavaScriptオブジェクトのクローンを扱うNode.jsバックエンドライブラリにも見つかっています。

2月21日には、Eran Hammerも、人気のバリデーションパッケージであり、エコシステム内の多くのプロジェクトで使われているjoiを通じてデータが漏えいし、プロトタイプ汚染の脆弱性がライブラリに及ぼす影響について自身の体験を共有しました。この脆弱性は、hapiウェブアプリケーションフレームワークを支える同じ開発者グループが開発したユーティリティライブラリ、hoekにも影響しました。

こうしたプロトタイプ汚染の脆弱性の多くは、Olivier Arteau(HoLyVieR)によって報告されました。Node.js Security WGが運営するHackerOneプログラムを通じた責任ある脆弱性開示で、JavaScriptエコシステム全体に影響するセキュリティ問題への対応とインシデント対応を支援しています。Oliverはプロトタイプ汚染の影響に関する詳細な脆弱性レポートも公開し、NorthSecカンファレンスではGhost CMSのNode.jsプロジェクトに影響した実例を発表しました。

まとめ

最後に、プロトタイプ汚染を防ぐため、以下の緩和策とセキュリティのベストプラクティスに従いましょう。

  • 安全な再帰的マージ処理を使用していることを確認してください。

  • プロトタイプ汚染攻撃の影響を受けないよう、Object.create(null)のように、プロトタイプを持たないオブジェクトの作成を検討してください。

  • ユーザーが制御するデータを扱う際は、角括弧表記を避けてください。可能であれば、常に避けるようにしましょう。マップベースの構造には、言語プリミティブのMapを使用することを検討してください。

Snykはセキュリティコミュニティを大切にしており、オープンソースパッケージの脆弱性を責任を持って開示することが、ユーザーのセキュリティとプライバシーの確保につながると考えています。オープンソースパッケージに脆弱性を発見した場合は、https://snyk.io/vulnerability-disclosureからご連絡ください。責任ある開示プログラムは、開発者と報告した研究者の双方を守りながら、研究者が発見した脆弱性を開発者が安全に活用できるよう支援します。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。