FlawOpen

flawopen.com/Teardowns/cve-2022-31150-undici-crlf-injection

● CVE-2022-31150 · CVSS 5.3 · 中
セキュリティ研究 · FlawOpen

CVE-2022-31150:Undici の CRLF 注入

CVE-2022-31150 では未検証の CR/LF が Undici のリクエスト生成に到達しました。上流の修正と更新方法を解説します。

💡 わかりやすい解説 (ELI5)

HTTP リクエストは、行の区切りが決まった用紙のようなものです。各行には一つの項目があります。未検証の文字列を転記すると、隠れた改行によって後続部分が新しい項目に見えることがあります。Undici の修正は、書き込む前にヘッダー値、メソッド、パスを検証します。

主要な概念と専門用語

Undici
Node.js 用 HTTP/1.1 クライアントライブラリ。Node.js の組み込み fetch にも Undici が含まれます。
CRLF 注入
信頼できない復帰・改行バイトがプロトコルのフィールドに入り、行境界を変えられる状態です。
HTTP token
ヘッダー名やメソッドなど、プロトコル要素に許される制限付き文字集合です。
リクエストターゲット
HTTP リクエストで送信するパスとクエリです。制御文字で構造を変えてはいけません。

フレームワーク対応

  • このガイドで説明する弱点の CWE 参照: CWE-93

根本原因の分析 (Root Cause)

修正前の Undici は、信頼できない入力に由来する不正な制御文字を含むヘッダー値をリクエストへシリアライズする可能性がありました。CR/LF は行区切りであり、受け入れるとヘッダー行を注入または破損できます。上流パッチはヘッダー名を HTTP token として検証し、値の不正文字を拒否し、メソッド token を検証し、パス内の制御文字を拒否します。GitHub アドバイザリは 5.8.0 未満を対象範囲とし、NVD の説明は 5.7.1 未満としています。どちらも修正版は 5.8.0 です。これはクライアントのリクエスト注入であり、SSRF や認証情報の露出が自動的に起きるわけではなく、アプリとネットワーク経路に依存します。

ステップ・バイ・ステップの攻撃フロー

ステップ 1

1. 未信頼の値

アプリのデータが Undici のヘッダー、メソッド、またはパスに渡ります。

ステップ 2

2. 検証不足

影響を受けるバージョンでは、不正な制御文字がリクエスト生成に入る可能性がありました。

ステップ 3

3. プロトコル境界

HTTP のシリアライズは行境界を使います。制御文字を受け入れるとフィールドを注入・破損できます。

ステップ 4

4. 修正と抑制

Undici 5.8.0 はシリアライズ前にヘッダーのバイト、メソッド token、パス文字を検証します。影響は状況によります。

ソースコード比較:脆弱 vs 堅牢化

脆弱な実装
// a29a151 適用前の lib/core/request.js にある該当シリアライズ箇所
function serializeRequest(request, method, path, headers) {
  request.method = method;
  request.path = path;
  for (const [key, value] of Object.entries(headers)) {
    // シリアライズ前にプロトコル文字を検証していない。
    request.headers += `${key}: ${value}\r\n`;
  }
}
堅牢化されたセキュアパッチ
// 上流リクエストパーサーの強化された検証パターン
const token = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
const invalidFieldValue = /[^\t\x20-\x7e\x80-\xff]/;
const invalidPath = /[^\u0021-\u00ff]/;

function validateRequestTarget(method, path) {
  if (!token.test(method)) throw new InvalidArgumentError('invalid request method');
  if (invalidPath.test(path)) throw new InvalidArgumentError('invalid request path');
}

function processHeader(request, key, value) {
  if (!token.test(key)) throw new InvalidArgumentError('invalid header key');
  if (invalidFieldValue.test(value)) throw new InvalidArgumentError(`invalid ${key} header`);
  request.headers += `${key}: ${value}\r\n`;
}

エンジニアリング&システム堅牢化チェックリスト

参考資料