この投稿では、JavaScriptで実装されたプラグインシステムにおいて脅威になりうる、プロトタイプ汚染攻撃の手法を説明します。
対策についてはまだ検証中なので説明しませんが、プラグインシステムを設計するにあたって、プロトタイプ汚染攻撃が脅威であることを一度整理しておきたかったので記事にしました。
プロトタイプ汚染攻撃とは
プロトタイプ汚染攻撃(prototype pollution attack)とは、ざっくり言うと、攻撃者がJavaScriptオブジェクトのプロトタイプを書き換えることで、情報を盗んだり、オブジェクトの振る舞いを変更したりする攻撃のことです。
JavaScriptの言語的特徴を巧妙に悪用した手口
JavaScriptでは、オブジェクトを生成すると、プロトタイプオブジェクトと呼ばれる生成元のオブジェクトへの関連を持つことになります。
例えば、Foo
オブジェクトを定義した場合、
class Foo {
doSomething() {
console.log('Hello World!')
}
}
Foo
オブジェクトにはprototype
というプロパティが生えて、そこにメソッドが入ります:
console.log(Foo.prototype.doSomething)
//=> [Function: doSomething]
Foo.prototype.doSomething()
//=> "Hello World!"
このFoo
オブジェクトをnew
すると、新しいオブジェクトを生成でき、
const foo = new Foo()
そのオブジェクトはdoSomething
メソッドを提供するようになりますが、
foo.doSomething()
//=> "Hello World!"
このメソッドは、実はFoo.prototype
のメソッドと同じものです:
console.log(Foo.prototype.doSomething === foo.doSomething)
//=> true
以上の動作を見ると分かりますが、Foo.prototype
とfoo
には関連があり、foo.doSomething
を呼び出した際には、その処理はFoo.prototype.doSomething
に移譲されているのです。
もしここで、Foo.prototype.doSomething
を置き換えると、Foo
をnew
して作られたオブジェクトのdoSomething
メソッドをまるっきし別の処理にすることができてしまいます:
// 置き換える処理
Foo.prototype.doSomething = function () {
console.log('Goodbye World!')
}
foo.doSomething()
//=> "Goodbye World!"
foo
はメソッド置き換え前にnew
されたものですが、それにも関わらず、後付で行われた置き換えの影響を受けているのが分かります。
以上のデモで分かったように、JavaScriptはプロトタイプを書き換えることが可能で、プロトタイプの書き換えは生成されたオブジェクトにも影響を与えることができるといった言語的特徴があるのです。
これは良く使えば、polyfillなどに活用できる便利でいい機能ですが、見方を変えると、特定の処理をぶんどることであらゆる悪用が可能な危険な機能でもあるのです。プロトタイプ汚染攻撃は、この言語的特徴を悪用した手口なのです。
プロトタイプ汚染攻撃はプラグインシステムで脅威になる
典型的なプロトタイプ汚染攻撃は、脆弱性があるサーバに対して一定のJSONを送ることで実現します。この典型的な攻撃についての解説は、他の文献に譲りたいと思います。
- Node.jsにおけるプロトタイプ汚染攻撃とは何か - ぼちぼち日記
- JavaScriptのプロトタイプ汚染攻撃対策は難しい - Qiita
- __proto__の除去でNode.jsのプロトタイプ汚染を防げないケース - knqyf263's blog
この典型的なプロトタイプ汚染攻撃を実現するためには、プロトタイプ汚染を引き起こしうる脆弱性がないとなりません。したがって、JSONをサーバに送りつけるような典型的な攻撃は、対応が難しいにしても、脆弱性を塞いでしまえば防衛できるはずです。
この投稿でテーマとしたいのは、プラグインシステムでのプロトタイプ汚染攻撃です。
プラグインシステムを持ったシステムはどいうものかというと、例えば、WordPressやATOMエディタです。WordPressやATOMなどは、コアの機能をプラグインを追加することで拡張することができます。
そして、プラグインは誰もが開発することができます。そして、ユーザは配布されたプラグインを自由にインストールすることができます。
すべてのプラグインが善良なものならいいですが、悪意を持ってプラグインを実装することも考えられます。プラグインは普通、制限なしに任意のコードが実行できるようになっているので、悪意を持ってプラグインを開発すると、プロトタイプ汚染攻撃は高い自由度を持って実現できてしまいます。(もちろん可能な悪用はプロトタイプ汚染だけに限られませんが、ここでは他の攻撃については取り上げません)
悪意を持ったプラグインに、プロトタイプ汚染攻撃のコードが仕込んであると、それに気づかずインストールしたユーザは機密情報を盗まれたりといった被害に遭うことが考えられます。
ユーザが信頼できるプラグインのみをインストールすることで、こうした攻撃を未然に防ぐことができるかも知れません。ユーザによる自衛です。ただ、これはユーザに一定の見定め能力を求めるので、どんなユーザが使うか分からないシステムでは、なかなか根本的な解決になりにくいと考えられます。
プラグインシステムを備えたアプリでは、
- プラグイン開発者が信頼できるとは限らず、
- ユーザの自衛に100%頼ることが難しい
そういったシステムは決して少なくないと思います。こうした環境があいまって、プラグインシステムではプロトタイプ汚染が脅威となりうると考えています。
プラグインシステムにおけるプロトタイプ汚染攻撃のデモ
プラグインシステムでどのようにプロトタイプ汚染攻撃が実現されるのか、単純なデモをやってみようと思います。
まず標的となる、機密データを扱う善良なオブジェクトInnocent
があるとします。
class Innocent {
handleSecretData(secret) {
console.log('善良なコードが機密データを受け取りました: %o', secret)
}
}
exports.Innocent = Innocent
システム本体ではこのInnocent
に機密データを渡す処理と、プラグインをロードする処理があるとします:
const { Innocent } = require('./innocent')
// プラグインをロードする処理
const fs = require('fs')
const files = fs.readdirSync('./plugins')
for (const file of files) {
require('./plugins/' + file)
}
// 機密データを扱う部分
const innocent = new Innocent()
innocent.handleSecretData('ひみつ')
かなりプラグインシステムを簡略化してはいますが、このコア機能では./plugins
に置かれたJSファイルをrequire
する形でプラグインのロードを行います。なので、ユーザは./plugins
にJSファイルを置くことで、システムにプラグインを追加することができます。
続いて、ユーザが何も知らずに、悪意あるプラグインをインストールしてしまったとします。そのプラグインのコードは次のように、Innocent
オブジェクトのプロトタイプのhandleSecretData
メソッドを上書きする形で、プロトタイプ汚染するようになっています:
const { Innocent } = require('../innocent')
// Innocentに対してプロトタイプ汚染するコード
const originalFunction = Innocent.prototype.handleSecretData
Innocent.prototype.handleSecretData = function (...args) {
console.log('悪意あるプラグインが機密データを盗みました: %o', args[0])
return originalFunction.apply(this, args)
}
この状態で、システム本体を動かすと、悪意あるプラグインがInnocent
オブジェクトの処理に割り込んで、機密データを盗んでいくようになります:
$ node system.js
悪意あるプラグインが機密データを盗みました: 'ひみつ'
善良なコードが機密データを受け取りました: 'ひみつ'
プラグインシステムにおけるプロトタイプ汚染の対策の理想形
プラグインシステムを設計する立場として理想形を考えると、
- ユーザの能力に頼ることもなく
- プラグインの開発者を認可制度などで制限することもなく
- プラグインのマーケットプレイスなどが安全性を評価することもなく
プロトタイプ汚染攻撃を最小化できる技術的な仕組みが欲しいところです。
そういう仕組みがあると、プラグインの開発が誰でも行え、自由に配布できるといった発展性があるエコシステムを維持したまま、ユーザはより安心してプラグインをインストールできるようになると思います。
具体的な対策については、考えているものがありますが、もう少し研究してから記事にしたいと思います。
もし何か対策アイディアがあればコメントなどで頂けると嬉しいです。