From 964d9ef78349496fc150e72f3fb93b1541192887 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Thu, 26 Mar 2026 07:00:28 -0400 Subject: [PATCH] Tests for strict header, trailer, and chunk ext parsing This needs an upstream PR. Signed-off-by: Demi Marie Obenour --- body_chunked.t | 220 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 219 insertions(+), 1 deletion(-) diff --git a/body_chunked.t b/body_chunked.t index 6d78d410..abff321b 100644 --- a/body_chunked.t +++ b/body_chunked.t @@ -6,6 +6,7 @@ ############################################################################### +use 5.36.0; use warnings; use strict; @@ -22,7 +23,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http proxy rewrite/)->plan(18); +my $t = Test::Nginx->new()->has(qw/http proxy rewrite/)->plan(2124); $t->write_file_expand('nginx.conf', <<'EOF'); @@ -135,6 +136,8 @@ like(http_get_body('/discard', '0123456789' x 128, '0123456789' x 512, 'chunked body discard 2'); # invalid chunks +use constant LF => "\x0A"; +use constant CR => "\x0D"; like( http( @@ -162,6 +165,221 @@ like( qr/400 Bad/, 'runaway chunk discard' ); +sub check_chunk ($chunk, $okay, $msg) { + my $rsp; + if ($okay) { + $rsp = qr/200 OK/; + } else { + $rsp = qr/400 Bad/; + } + like( + http( + 'GET /discard HTTP/1.1' . CRLF + . 'Host: localhost' . CRLF + . 'Connection: close' . CRLF + . 'Transfer-Encoding: chunked' . CRLF . CRLF + . '8' . CRLF + . 'SEE-THIS' . CRLF + . '0' . $chunk . CRLF . CRLF + ), + $rsp, $msg + ); +} + +sub check_hdr_char ($hdr_byte, $okay, $msg) { + my $rsp; + if ($okay) { + $rsp = qr/200 OK/; + } else { + $rsp = qr/400 Bad/; + } + like( + http( + 'GET /discard HTTP/1.1' . CRLF + . 'Host: localhost' . CRLF + # If this is LF the next line will be an empty header, + # which is invalid. + . "$hdr_byte: ignored". CRLF + . 'Connection: close' . CRLF . CRLF + ), + $rsp, 'header ' . $msg + ); + like( + http( + 'GET /discard HTTP/1.1' . CRLF + . 'Host: localhost' . CRLF + . 'Transfer-Encoding: chunked' . CRLF + . 'Connection: close' . CRLF . CRLF + . '0' . CRLF + # If this is LF the next line will be an empty trailer, + # which is invalid. + . "$hdr_byte: ignored" . CRLF + . 'ignored: ignored' . CRLF . CRLF + ), + $rsp, 'trailer ' . $msg + ); +} + +sub get_regex_for_okay($is_okay) { + $is_okay ? qr/200 OK/ : qr/400 Bad/; +} + +sub get_packed_and_hex ($value) { + unless (wantarray) { + die 'Must be called in list context'; + } + my $packed_string = pack('C', $value); + my $hex_byte = ($value > 0x20 && $value < 0x7F) ? + $packed_string : + sprintf '0x%02x', $value; + return $packed_string, $hex_byte; +} + +foreach my $hdr_byte (0..255) { + my ($packed, $hex) = get_packed_and_hex($hdr_byte); + my $okay = $hdr_byte >= 0x20 ? $hdr_byte != 0x7F : $hdr_byte == 0x9; + my $msg = $okay ? 'good' : 'bad'; + like( + http( + 'GET /discard HTTP/1.1' . CRLF + # Use '"something' to trigger an error if this is a line feed + . 'Ignored: ' . $packed . '"something' . CRLF + . 'Connection: close' . CRLF + . 'Host: localhost' . CRLF . CRLF + ), + get_regex_for_okay($okay), $msg . ' byte in header value ' . $hex + ); + like( + http( + 'GET /discard HTTP/1.1' . CRLF + . 'Transfer-Encoding: chunked' . CRLF + . 'Connection: close' . CRLF + . 'Host: localhost' . CRLF . CRLF + . '0' . CRLF + # Use '"something' to trigger an error if this is a line feed + . 'Ignored: ' . $packed . '"something' . CRLF + . 'Ignored-2: ignored' . CRLF . CRLF + ), + get_regex_for_okay($okay), $msg . ' byte in trailer value ' . $hex + ); + like( + http( + 'GET /discard HTTP/1.1' . CRLF + . 'Transfer-Encoding: chunked' . CRLF + . 'Connection: close' . CRLF + . 'Host: localhost' . CRLF . CRLF + . "0;a=\"\\$packed\"" . CRLF . CRLF + ), + get_regex_for_okay($okay), $msg . ' byte in chunk extension value ' . $hex + ); + like( + http( + 'GET /discard HTTP/1.1' . CRLF + . 'Transfer-Encoding: chunked' . CRLF + . 'Connection: close' . CRLF + . 'Host: localhost' . CRLF . CRLF + . "0;a=\"$packed\"" . CRLF . CRLF + ), + get_regex_for_okay($okay && $packed ne '\\' && $packed ne '"'), + $msg . ' byte in unescaped chunk extension value ' . $hex + ); +} + +my $token_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&\'*+.^_-`|~'; +for my $value (0..255) { + my ($packed, $hex) = get_packed_and_hex($value); + my $is_token = index($token_chars, $packed) != -1; + my $msg = $is_token ? '' : ' not'; + check_chunk("; $packed=b", $is_token, "token parsing for byte $hex (is$msg token)"); + check_chunk("; a=b${packed}c", $is_token, "token parsing for byte $hex (is$msg token)"); + check_hdr_char($packed, $is_token, "header name parsing for byte $hex (is$msg token)"); +} +check_hdr_char('', 0, 'name is empty'); +foreach my $bad ( LF . CRLF, CRLF . LF, LF . LF, CR . CR, LF . CR, LF, CR ) { + my $hex = unpack('H*', $bad); + like( + http( + 'GET /discard HTTP/1.1' . CRLF + . 'Host: localhost' . CRLF + . 'Transfer-Encoding: chunked' . CRLF + . '0' . $bad + ), + qr/400 Bad/, 'bare LF in chunk encoding ' . $hex + ); + like( + http( + 'GET /discard HTTP/1.1' . CRLF + . 'Host: localhost' . CRLF + . 'Transfer-Encoding: chunked' . CRLF + . 'Connection: close' . CRLF . CRLF + . '8' . $bad + . 'SEE-THIS' . CRLF + . '0' . CRLF . CRLF + ), + qr/400 Bad/, 'bare LF in chunk encoding (nonzero length) ' . $hex + ); + like( + http( + 'GET /discard HTTP/1.1' . CRLF + . 'Host: localhost' . CRLF + . 'Transfer-Encoding: chunked' . CRLF + . 'Connection: close' . CRLF . CRLF + . '0' . CRLF + . 'a: b' . $bad . CRLF + ), + qr/400 Bad/, 'bare LF trailers' + ); +} + +check_chunk('', 1, 'no ext'); +check_chunk(';a=b', 1, 'good simple ext'); +check_chunk(';', 0, 'no ext after semi'); +check_chunk(';b', 0, 'no equal'); +check_chunk(';b=', 0, 'no value'); +check_chunk(' ;a=b', 1, 'bws before semi'); +check_chunk(' ', 0, 'bws no ext'); +foreach my $bws (' ', "\t") { + check_chunk("$bws;a=b", 1, 'bws'); + check_chunk(";${bws}a=b", 1, 'bws'); + check_chunk(";a$bws=b", 1, 'bws'); + check_chunk(";a=${bws}b", 1, 'bws'); + check_chunk(";a=b${bws}", 0, 'bws after val'); +} +check_chunk(';a="', 0, 'unterminated quote'); +check_chunk(';=a', 0, 'empty name'); +check_chunk(';a=', 0, 'empty value'); +check_chunk(';a=""', 1, 'empty quoted value'); +check_chunk(";a=\"\0\"", 0, 'bad char'); +check_chunk(";a=\"\\a\"", 1, 'quoted string'); +check_chunk(';a="\\""', 1, 'quoted string'); +check_chunk(';a="\\"', 0, 'missing quote'); +check_chunk(';a="\\"";b=c', 1, 'two good exts'); + +sub check_trailer ($trailer, $okay, $msg) { + like( + http( + 'GET /discard HTTP/1.1' . CRLF + . 'Host: localhost' . CRLF + . 'Connection: close' . CRLF + . 'Transfer-Encoding: chunked' . CRLF . CRLF + . '8' . CRLF + . 'SEE-THIS' . CRLF + . '0' . CRLF . $trailer . CRLF . CRLF + ), + get_regex_for_okay($okay), $msg + ); +} + +check_trailer('a: b', 1, 'good trailer'); +check_trailer('\\: b', 0, 'bad char in name'); +check_trailer("a: \0", 0, 'bad char in value'); +check_trailer("ab", 0, 'no colon'); +check_trailer(' ab: c', 0, 'leading space'); +check_trailer('content-length: 10000', 0, 'content-length in trailer'); +check_trailer('transfer-encoding: chunked', 0, 'transfer-encoding in trailer'); +check_trailer('upgrade: chunked', 0, 'upgrade in trailer'); +check_trailer('pgrade: chunked', 1, 'okay trailer'); + # proxy_next_upstream like(http_get_body('/next', '0123456789'),