
セキュリティアドバイザリ: 未解析のTransfer-Encoding値によるHTTPリクエストスマグリング (tiny_http)
割り当て済みCVE ID: CVE-2026-66752
tiny_httpは、Transfer-Encodingヘッダーが存在するかどうかのみをチェックし、その値を一切確認しません。chunkedではないコーディングや、最後の要素がchunkedではないコーディングリストを含む、あらゆる値がボディのチャンクデコードを引き起こし、同時にContent-Lengthは破棄されます。
転送コーディングを正しく解析するフロントエンドは、このようなリクエストをtiny_httpとは異なるフレーミングで処理します。1つの接続上で2つの異なるフレーミングが存在することが、リクエストスモグリングの前提条件です。また、非チャンクのコーディングで非チャンクのボディを送信すると、tiny_httpはボディの読み取りに失敗し、応答を一切返しません。
リポジトリURL: https://github.com/tiny-http/tiny-http
| 影響あり | 0.12.0(2022-10-06、現行リリース)を含む、リリース済みの全バージョン |
| 検証済み | 0.6.2, 0.6.3, 0.8.0, 0.9.0, 0.10.0, 0.11.0, 0.12.0 |
| 修正版 | 執筆時点では修正版なし |
CWE-444(HTTPリクエストの不整合な解釈)。
CVSS 4.0 ベーススコア 6.3(Medium)
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:L/SI:L/SA:N
リモートから生のHTTPを送信する、認証されていないクライアント。認証情報やユーザー操作は不要です。
スモグリングのケースでは、tiny_httpが、Transfer-EncodingとContent-Lengthを転送し、RFC 9112のセクション6.1を正しく適用するフロントエンドまたはCDNの背後に配置されている必要があります。つまり、最後の要素がchunkedではないコーディングリストをチャンクではないものとして扱い、Content-Lengthにフォールバックするものが必要です。そのフロントエンドとtiny_httpは、ボディの終端について異なる解釈をします。
応答が返らないリクエストのケースは、接続できること以外に何も必要としません。
tiny_http-0.12.0/src/request.rsの143行目から153行目。ヘッダーは検索されクローンされますが、値は存在するかどうかのみが検査されます:
143 // finding the transfer-encoding header
144 let transfer_encoding = headers
145 .iter()
146 .find(|h: &&Header| h.field.equiv("Transfer-Encoding"))
147 .map(|h| h.value.clone());
148
149 // finding the content-length header
150 let content_length = if transfer_encoding.is_some() {
151 // if transfer-encoding is specified, the Content-Length
152 // header must be ignored (RFC2616 #4.4)
153 None
tiny_http-0.12.0/src/request.rsの218行目から221行目で、チャンクデコーダーを無条件に適用します:
218 } else if transfer_encoding.is_some() {
219 // if a transfer-encoding was specified, then "chunked" is ALWAYS applied
220 // over the message (RFC2616 #3.6)
221 Box::new(FusedReader::new(Decoder::new(source_data))) as Box<dyn Read + Send + 'static>
transfer_encodingは、生の値を保持するOption<AsciiString>であり、その内容を検査する処理は一切ありません。RFC 9112のセクション6.1は、リクエストの最終コーディングがchunkedであることを要求し、それ以外の場合はサーバーがメッセージを拒否することを要求しています。また、セクション6.3は、Transfer-EncodingとContent-Lengthの両方を保持するリクエストを拒否することを要求しています。
ステップ1. フレーミングヘッダーと読み取ったボディを報告するサーバーを起動します。
use std::io::Read;
use tiny_http::{Response, Server};
fn main() {
let server = Server::http("127.0.0.1:8005").unwrap();
for mut request in server.incoming_requests() {
let te = request.headers().iter()
.find(|h| h.field.equiv("Transfer-Encoding"))
.map(|h| h.value.as_str().to_string());
let cl = request.headers().iter()
.find(|h| h.field.equiv("Content-Length"))
.map(|h| h.value.as_str().to_string());
let mut body = Vec::new();
let r = request.as_reader().read_to_end(&mut body);
println!("TE={:?} CL={:?} read={:?} body={:?}",
te, cl, r, String::from_utf8_lossy(&body));
let _ = request.respond(Response::from_string("ok"));
}
}
ステップ2. Transfer-Encoding: identity、一致するContent-Length、およびプレーンなボディを持つリクエストを送信します。どのフロントエンドでも、このボディは5バイトのhelloとしてフレーミングされます。
printf 'POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: identity\r\nContent-Length: 5\r\n\r\nhello' | nc -w 2 127.0.0.1 8005
結果。tiny_httpはチャンク形式ではないボディをチャンクデコードして破棄し、応答を送信しませんでした。クライアントは自身のタイムアウトまで待機し、接続は消費されます。
TE=Some("identity") CL=Some("5") read=Err(Custom { kind: InvalidInput, error: DecoderError }) body=""
ステップ3. 最後の要素がchunkedではないコーディングリストを送信します。
printf 'POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked, identity\r\n\r\n5\r\nhello\r\n0\r\n\r\n' | nc -w 2 127.0.0.1 8005
結果。tiny_httpはそれを受理し、チャンクとしてデコードします。RFC 9112のセクション6.1では拒否が要求されています:
TE=Some("chunked, identity") CL=None read=Ok(5) body="hello"
対照的に、Content-Lengthのみの場合と正しいTransfer-Encoding: chunkedの場合はどちらも適切に動作し、body="hello"が得られます。
tiny_httpが転送コーディングを正しく解析するフロントエンドの背後に配置されている場合、2つのコンポーネントはリクエストボディの終端について異なる解釈をします。攻撃者はその解釈の不一致の両側のバイトを選択できます。これは、フロントエンドのルーティングとアクセス制御を迂回してリクエストをスモグリングするための標準的な構成です。
フロントエンドとは無関係に、ステップ2と3は、ワーカーを占有し、応答を一切受け取らないごく単純な不正なリクエストを示しています。そのため、クライアントはリクエストが拒否されたことを認識できません。
src/request.rsの144行目付近で、Transfer-Encodingを、それが実際にはカンマ区切りリストであるものとして解析します:
ボディを保持するリクエストについては、最終コーディングがchunkedであることを要求し、それ以外のコーディングはチャンクデコードするのではなく400で拒否します。RFC 9112のセクション6.3がプロキシではないサーバーに要求しているとおり、Transfer-EncodingとContent-Lengthの両方を保持するリクエストは、150行目でContent-Lengthを黙って無視するのではなく拒否します。
ボディリーダーが失敗した場合は、応答なしでリクエストを破棄するのではなく、400を送信します。