SSTによる安全なWebサイト運営のためのセキュリティ情報

エンジニアブログ
  • shareSNSでシェア
  • Facebookでシェアする
  • Xでシェアする
  • Pocketに投稿する
  • はてなブックマークに投稿する

Node.jsにおけるprototype汚染攻撃への対策

はじめに

こんにちは、CTOのはせがわようすけです。

少し前に大津さんが Node.js におけるprototype汚染攻撃を紹介する記事を掲載されていました。

Node.jsにおけるプロトタイプ汚染攻撃とは何か

どういう原理での攻撃なのかの解説は大津さんの記事を参照頂くとして、記事内で紹介されている講演の動画では最終的に任意コード実行まで至っているという点で非常に興味深いものがあります。
攻撃の経路としてはクライアントからHTTP経由でJSONをPOSTするというだけですので、いくら Object.prototype を上書きできたとしても送ることのできるデータはJSONで表現可能なプリミティブな型のみで、JavaScriptの関数は含めることはできません。
この講演動画で扱われているGhost CMSというソフトウェアでは、__proto__ 経由でテンプレートのファイル名だけでなくその中で実行されるコードも文字列で指定できたということで、結果的にリモートからの任意コード実行につながっているようです。

prototypeを書き換えつつアプリケーションをクラッシュせずに、さらに文字列からのコード生成まで行うことのできるソフトウェアというのは、何となくそれほど多くないような気もしますが、実際のところはそんなことはないのかも知れません。

プロトタイプ汚染への対策

この講演では対策として

  • Object.freeze() を使う。副作用が出る可能性がある。
  • JSON schema を使う。
  • Object ではなく Map を使う。

の3つが挙げられています。Object.freeze() や JSON schema はともかくとして、 Object の代わりに Map を使うというのは、あくまでも個人的な感覚ですが、コードをコンパクトに書けなくなったり、既存のコードで頻繁に使われているObjectをMapに置き換えるのは相当なコストがかかったりと、あまり現実的ではないような気がします。

元の発表者が書かれた論文では、上記3種の対策方法に加えてオブジェクトリテラル {} ではなく Object.create(null) を使うことでprorotypeを持たないオブジェクトを生成するという方法も紹介されています。

prototype-pollution-nsec18/paper at master · HoLyVieR/prototype-pollution-nsec18

Object.create() に null を与えた場合、Object.prototype を引き継がないオブジェクトを生成できます。これにより、 __proto__ が与えられた時でも Object.prototype へ影響を与えることが無くなります。

const obj1 = Object.create(null);
console.log(obj1.__proto__ === Object.prototype); // false
obj1.__proto__.polluted = 1; // obj1.__proto__ が undefined なのでエラー
const obj2 = Object.create(null);
console.log(obj2.polluted); 

外部由来のデータを扱い __proto__が挿入される可能性のあるオブジェクト — 上のコードの例ではobj1 — を Object.create(null) で生成しておくことで、攻撃者が __proto__ を作成しても Object.prototype には触れることはできなくなります。また、prototype汚染の影響を受けてしまう、プログラム内でデータとして取り扱うオブジェクト — 上のコードの例ではobj2 — も同様に Object.create(null) で作成しておくことで、万が一 Object.prototype が汚染されていた場合でも、 prototype チェインを辿って攻撃者が作成した Object.prototype へのアクセスを防ぐことができます。

大津さんのブログ記事の「4. 実際の攻撃」の脆弱性があるサーバーのコードにこれを適用するとすれば、以下のようなコードになります。

function clone(obj) {
  return merge(Object.create(null), obj);  // オブジェクトを複製する際にオブジェクトリテラルをベースにするのではなくprorotypeを持たないオブジェクトをベースにするよう変更
}

外部由来の __proto__ が入り込むオブジェクトである↑の部分だけでも対策は可能ですが、さらにデータとして使用される他のオブジェクトを生成する箇所↓も変更しておくとより安心できます。

app.post('/', (req, res) => {
  const obj = clone(req.body);
  const r = Object.create(null);   // r.prototype は undefined になるため、仮に prototype が汚染されていても攻撃の攻撃の影響を受けない
  const status = r.status ? r.status: 'NG';
  res.send(status)
});

Object.create(null) によるオブジェクトの生成は、空のオブジェクト {} であればシンプルに Object.create(null) を呼び出すだけで済みますが、{a:1, b:2} のようにいくつかのプロパティをもつオブジェクトを生成するという場合には Object.assign() と組み合わせてオブジェクトを生成する必要があります。

const obj = Object.assign(Object.create(null), {a:1, b:2});

また、このようにして生成されたオブジェクトは prototype が Object.prototype をさしていないので、prototype チェインを辿って Object.prototype 内のメソッドを呼び出すことはできません。

const obj = Object.create(null);
obj.a = 1;
obj.hasOwnProperty('a'); // TypeError: obj.hasOwnProperty is not a function

hasOwnProperty などを呼び出す際は Object.prototype.hasOwnProperty を明示的に呼び出すようにする必要があります。

const obj = Object.create(null);
obj.a = 1;
Object.prototype.hasOwnProperty.call(obj, 'a'); // true

このように、オブジェクトリテラルに代えて Object.create(null) を使う方法は、講演で紹介されている「Objectに代えてMapを使う」という方法に比べれば変更は少ないものの、相応の副作用などもあり、機械的にコードを置き換えるだけで済むというわけではありません。

他のアプローチによる対策

上で説明したようにオブジェクトリテラルに代えてObject.create(null) によってオブジェクトを生成することで prototype 汚染を防ぐことはできましたが、既存のソースコード全てにおいて影響範囲を調べ上記のような書き換えを行うのは現実的ではありませんし、サードパーティのモジュールが安全にオブジェクトリテラルを使用しているかどうかを調べるのも困難です。そこで、もう少し ad-hoc に既存コードを安全にする方法を考えます。

攻撃は、HTTP経由やファイル、データベースなど攻撃者が加工できる箇所から受け取ったJSON文字列を元にオブジェクトが生成され(この時点ではまだ Object.prototype は汚染されていない)、そのオブジェクトのプロパティが他のオブジェクトリテラルによって生成された他のオブジェクトにコピーされることで汚染が発生します。
つまり攻撃者が細工できる攻撃の入り口としては、ほとんどの場合はJSON形式の文字列であるため、JSON.parse を書き換えて、生成されるオブジェクトから __proto__ をキー名にもつプロパティは除外するよう JSON.parse の挙動を変えてしまうことで、攻撃のほとんどのケースを無効化できそうです。

具体的には、以下のようなグローバルな JSON.parse を上書きするコードをプログラムの起動時に呼び出し、以降の JSON.parse 呼び出しで __proto__ がオブジェクトに含まれないようにします。

JSON._parse = JSON.parse;
JSON.parse = (text, reviver) => {
  const removeProto = (obj) => {
    for (let key in obj) {
      // キー名が __proto__ のときはオブジェクトからプロパティを削除する
      if (key === '__proto__') {
        delete obj[key];
      } else if (typeof obj[key] === 'object' && obj[key] !== null) {
        removeProto(obj[key]);
      }
    }
  }
  // まずは一旦オリジナルの JSON.parse を呼び出す
  const r = JSON._parse(text, reviver);
  // 生成されたオブジェクトから __proto__ を削除する
  removeProto(r);
  return r;
};

正常なJSON文字列に __proto__ のようなキー名が含まれるとは考えにくく、これで副作用が発生することは通常はないのではないでしょうか。

この方法では、既存のコードに手を加えることなく、サードパーティモジュールも含めて潜在的に prototype 汚染攻撃の問題を持つコードに対して(内部的にJSON.parseを使ってさえいれば)透過的に保護することができます。もちろん、JSON.parse を通ることなく攻撃者がオブジェクトを生成し __proto__ プロパティを設定できてしまうケースにおいてはこの対策は役には立ちませんが、通常そういうケースは少ないのではないかという気がします。

終わりに

今回は Node.js における prototype 汚染攻撃の対策について検討してみましたが、既存のコードへの影響を抑えつつ簡単かつ確実に攻撃を防ぐことは相当難しいということがわかりました。
JSON.parse の書き換えも実サービスで踏み切るのは影響を考えると躊躇してしまうのではないでしょうか。現実的には、外部から受け取ったJSONに対して、安直に全てのkeyを列挙して使うのではなく、その中には __proto__ のように動作に影響を与えるキーも含まれているということを意識する、言い換えると「気をつける」という程度の精神的な対策となってしまうのかも知れません。

もし当記事を読んで間違いに気づいた場合やよりいい方法を思いついたという場合は、ぜひ @hasegawayosuke まで教えて頂けると助かります。

このように「発見されたセキュリティ問題について開発者に対してどのように現実的な対策を提供できるのかを一緒に考えられるエンジニア」を弊社では募集しています。履歴書お待ちしております。

  • shareSNSでシェア
  • Facebookでシェアする
  • Xでシェアする
  • Pocketに投稿する
  • はてなブックマークに投稿する

この記事の筆者

筆者

はせがわようすけ

(株)セキュアスカイ・テクノロジー 取締役CTO