
割り当て済みCVE ID: CVE-2026-66753
tiny_httpは、HTTPヘッダー値内のキャリッジリターン(CR)やラインフィード(LF)を、リクエスト側・レスポンス側のどちらでも拒否しません。
リクエスト側では、read_next_line はCRLFの場合にのみヘッダー行を終了するため、単独のLFはパースされた値の中に残り、アプリケーションに到達します。レスポンス側では、Header::from_bytes はバイトがASCIIであることだけを検証し、レスポンスライターは値をそのまま出力するため、CRLFを含む値はレスポンスを分割します。
リクエストヘッダー値をレスポンスヘッダーに反映するアプリケーションや、リクエストヘッダーを別の接続に再シリアライズするアプリケーションは、何か問題があることを示す兆候なしに、インジェクションのプリミティブを継承することになります。
リポジトリ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 |
| 修正版 | 執筆時点では修正版なし |
これは、Transfer-Encoding のパースを対象とし0.6.3と0.8.0で修正された RUSTSEC-2020-0031 / CVE-2020-35884 とは異なります。ここで説明する動作は、その修正の前後両方に存在します。
CWE-113 (HTTPヘッダーにおけるCRLFシーケンスの不適切な無効化) は、CWE-93 (CRLFインジェクション) の特定のケースです。
CVSS 4.0 基本スコア 6.3 (Medium)
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N
任意のtiny_httpサーバーに生のHTTPを送信する、リモートかつ認証不要のクライアント。認証情報やユーザーの操作は不要です。
この問題を脆弱性にするのは、次の2つの利用形態です:
リクエストヘッダーの値をレスポンスヘッダーにコピーするアプリケーション。これはCORSオリジンエコー、リクエストデータから構築される Location ヘッダー、およびクッキー処理の背後にあるリフレクションパターンです。リクエスト値内の単独のLFはレスポンスに到達し、アプリケーションがレスポンス値を自ら構築する場合、完全なCRLFを運ぶことができます。
リバースプロキシのように、リクエストヘッダーを別の接続に再シリアライズするアプリケーション。単独のLFはアップストリームリクエストに書き込まれ、裸のLFをヘッダー行の終端として扱うバックエンドは、1つのリクエストを2つとして読み取ります。RFC 9112の2.2節は受信者がこれを行うことを許可しているため、そのようなバックエンドは仕様の範囲内です。Goの net/http とPythonの http.server はどちらもこれを受け入れることが測定で確認されています。
tiny_http-0.12.0/src/client.rs の80行目から102行目。このループは、LFの前にCRがある場合にのみ戻ります。単独のLFや単独のCRを含むその他のバイトはすべて、100行目でラインバッファにプッシュされます:
84 loop {
85 let byte = self.next_header_source.by_ref().bytes().next();
...
92 if byte == b'\n' && prev_byte_was_cr {
93 buf.pop(); // removing the '\r'
94 return AsciiString::from_ascii(buf)
95 .map_err(|_| IoError::new(ErrorKind::InvalidInput, "Header is not in ASCII"));
96 }
97
98 prev_byte_was_cr = byte == b'\r';
99
100 buf.push(byte);
101 }
LFは0x0A、CRは0x0Dで、どちらも有効なASCIIであるため、94行目の AsciiString::from_ascii はこれらを受け入れます。
続いて tiny_http-0.12.0/src/common.rs の184行目から191行目は、値をトリムするだけです。trim は先頭と末尾の空白を削除しますが、内部のバイトはそのまま残します:
184 fn from_str(input: &str) -> Result<Header, ()> {
185 let mut elems = input.splitn(2, ':');
186
187 let field = elems.next().and_then(|f| f.parse().ok()).ok_or(())?;
188 let value = elems
189 .next()
190 .and_then(|v| AsciiString::from_ascii(v.trim()).ok())
191 .ok_or(())?;
CRLFのペアは行を終了させるため残りませんが、単独のLF、単独のCR、および \n\r のようなシーケンスはすべて残る可能性があります。
tiny_http-0.12.0/src/common.rs の166行目から172行目は、ASCIIのみをチェックします:
166 pub fn from_bytes<B1, B2>(header: B1, value: B2) -> Result<Header, ()>
...
171 let header = HeaderField::from_bytes(header).or(Err(()))?;
172 let value = AsciiString::from_ascii(value).or(Err(()))?;
tiny_http-0.12.0/src/response.rs の99行目から104行目は、値をエスケープなしで書き込みます:
99 for header in headers.iter() {
100 writer.write_all(header.field.as_str().as_ref())?;
101 write!(&mut writer, ": ")?;
102 writer.write_all(header.value.as_str().as_ref())?;
103 write!(&mut writer, "\r\n")?;
104 }
226行目の HeaderField::from_str はフィールド名の空白を拒否しますが、171行目の HeaderField::from_bytes は拒否しないため、レスポンスヘッダー名もチェックされないことに注意してください。
手順1. パースされたリクエストヘッダー値を出力し、CRLFを含む値をレスポンスヘッダーにエコーするサーバーを起動します。
use tiny_http::{Header, Response, Server};
fn main() {
let server = Server::http("127.0.0.1:8004").unwrap();
for request in server.incoming_requests() {
for h in request.headers() {
if h.field.equiv("X-Test") {
println!("parsed X-Test value = {:?}", h.value.as_str());
}
}
let evil = "a\r\nX-Injected: yes";
let mut resp = Response::from_string("body");
resp.add_header(Header::from_bytes(&b"X-Echo"[..], evil.as_bytes()).unwrap());
let _ = request.respond(resp);
}
}
手順2. X-Test の値に裸のLFを含むリクエストを送信します。以下のコマンドでは、\n は単一のラインフィード、\r\n はCRLFです。この区別がテストの要点なので、エディタに正規化させないでください。
printf 'GET / HTTP/1.1\r\nHost: x\r\nX-Test: aaa\nbbb\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8004
stdout上の結果。LFはパースされた値の中に依然として残っています:
parsed X-Test value = "aaa\nbbb"
ワイヤー上の結果。レスポンスヘッダーが2つに分割されました:
HTTP/1.1 200 OK
Server: tiny-http (Rust)
Content-Type: text/plain; charset=UTF-8
X-Echo: a
X-Injected: yes
Content-Length: 4
body
tiny_http自体はリクエストヘッダーを再出力しないため、リクエスト側の動作は直接的な侵害ではなく潜在的なプリミティブです。これは、ヘッダー値を転送または反映する任意の利用者において悪用可能になり、LF耐性のあるバックエンドに対するリクエストスマグリングや、レスポンス内のヘッダーインジェクションを引き起こします。
レスポンス側の動作は、攻撃者の影響を受けるテキストをヘッダー値に配置する任意のアプリケーションで直接悪用可能です: オリジンのコンテキストでのスクリプトインジェクション、キャッシュポイズニング、注入された Set-Cookie によるセッションフィクセーション、およびセキュリティヘッダーの上書きです。
比較として、http クレートは HeaderValue::from_str でCRとLFを拒否するため、それに基づいて構築されたスタックはどちらの動作も公開しません。
両側の境界で制御文字を拒否します。
read_next_line (src/client.rs の80行目) では、ヘッダー行内の裸のCRまたは裸のLFを値に含めるのではなく、プロトコルエラーとして扱い400を返します。あるいは、分割後に Header::from_str (src/common.rs の184行目) でそれらを拒否します。
Header::from_bytes (src/common.rs の166行目) では、0x0D、0x0A、または0x00を含む値を拒否し、RFC 9110のトークンセット以外を含むフィールド名を拒否します。シグネチャが失敗可能 (フォーリブル) であるため、そこで Err を返すことはすでに呼び出し側で処理されています。