nodemailerのaddressparser:RFC 5322のコメントを含むアドレスを処理する際に、O(n^2)の計算時間となり、リモートDoS攻撃(mailparser経由で到達可能)を引き起こす可能性があります。
### 概要
`nodemailer/lib/addressparser`は、特定の形式のアドレスをO(n^2)の時間で解析します。約640KBのサイズの単一のアドレス値が、Node.jsのイベントループを約7秒間ブロックします。また、認証なしでアクセス可能です。mailparserは受信メールのヘッダーをこのパーサーに直接入力するため、巧妙に作成された単一のメールで、メールを解析するサービスを停止させることができます。
### 詳細
このパーサーは、アドレスを構成する要素を単一の文字列に結合することで、アドレスを構築します。RFC 5322のコメントによって要素が区切られている場合、例えば`a@b(c)@b(c)@b(c)...`のような場合、各要素が同じ文字列に結合されます。
`src/addressparser/index.ts`の結合チェック:
```js
const joins =
prevToken &&
prevToken.noBreak &&
parts.length &&
(prevToken.value !== ')' || parts[parts.length - 1].slice(-1) === '@' || token.value.charAt(0) === '@');
```
問題は、最後の2つのオペランドの順序です。`parts[parts.length - 1].slice(-1)`が、安価な`token.value.charAt(0)`の前に実行されます。`slice(-1)`は、蓄積された文字列の最後の文字を読み取るために、文字列全体を展開する必要があります(O(現在の長さ))。これがすべてのトークンに対して実行されるため、全体の値に対してO(n^2)の計算時間となります。`||`は左から右に評価されるため、短絡評価される可能性のある安価な`charAt(0)`が実行されることはありません。
この連鎖:長いコメントを含むアドレス → 1つの文字列への結合 → 各トークンで`slice(-1)`が文字列を展開 → 二次的な解析時間。
### 実証
パーサーのみを分離して実行します(`npm i nodemailer@10.0.3`):
```js
const addressparser = require('nodemailer/lib/addressparser');
const s = Date.now();
addressparser('a' + '@b(c)'.repeat(130000));
console.log(Date.now() - s, 'ms'); // 約7000ms、ブロック状態
```
mailparser経由でのエンドツーエンドのテスト(`npm i mailparser@3.9.24`):
```js
const { simpleParser } = require('mailparser');
(async () => {
const to = 'a' + '@b(c)'.repeat(130000);
const eml = `From: a@b.com\r\nTo: ${to}\r\nSubject: x\r\n\r\nhi\r\n`;
const s = Date.now();
await simpleParser(eml);
console.log(Date.now() - s, 'ms'); // 約7000ms、処理がブロックされる
})();
```
バージョン10.0.3での測定結果:
| アドレスのサイズ | 解析時間 |
| --- | --- |
| 390 KB | 1.3秒 |
| 585 KB | 5.6秒 |
| 640 KB | 6.7秒 |
| 976 KB | 18秒 |
RFC 5322の折り返しにも対応しています。ヘッダーは、アトムの長さの倍数となるオフセットで折り返し、すべての`)`と`@`の組み合わせがそのまま維持されるため、ペイロードは998オクテット以下の行を含む、規格に準拠したメールであり、それでも問題を引き起こします。
### 影響
アルゴリズムの複雑性によるDoS攻撃。Node.jsはシングルスレッドであるため、この処理がブロックされると、プロセス内の他のすべての処理が停止します。このような攻撃が連続して行われると、サービスが停止する可能性があります。
影響を受けるのは、攻撃者が制御する入力に対して`addressparser`を実行するすべてのもの、つまり、パブリックなエクスポートを直接使用する場合、またはユーザー入力から構築されたアドレスヘッダーを使用する場合です。認証されていないリモート環境では、`mailparser`がnodemailer 10.0.3を使用しており、受信した`To`/`From`/`Cc`ヘッダーに対して長さ制限なしでパーサーを呼び出すため、それらを使用しているサービスは、単一のメールによってフリーズする可能性があります。
### 修正案
最後の2つのオペランドを入れ替えて、安価なチェックを最初に実行するようにします。
```js
(prevToken.value !== ')' || token.value.charAt(0) === '@' || parts[parts.length - 1].slice(-1) === '@')
```
これは純粋なブール演算の置換であり、解析結果は同じです。テスト入力に対してバイト単位で検証されており、1276件のテストスイート全体が合格しています。
影響を受けるパッケージ:
- npm (JavaScript) nodemailer: 影響あり >= 9.1.0, <= 10.0.4、10.0.5で修正済み。