Medium severity5.3NVD Advisory· Published May 3, 2026· Updated May 7, 2026
CVE-2026-40561
CVE-2026-40561
Description
Starlet versions through 0.31 for Perl allows HTTP Request Smuggling via Improper Header Precedence.
Starlet incorrectly prioritizes "Content-Length" over "Transfer-Encoding: chunked" when both headers are present in an HTTP request. Per RFC 7230 3.3.3, Transfer-Encoding must take precedence.
An attacker could exploit this to smuggle malicious HTTP requests via a front-end reverse proxy.
Affected products
2Patches
1a7d5dfd1862aPrevent HTTP Smuggling.
3 files changed · +131 −0
lib/Starlet/Server.pm+15 −0 modified@@ -296,6 +296,17 @@ sub handle_connection { $buf = substr $buf, $reqlen; my $chunked = do { no warnings; lc delete $env->{HTTP_TRANSFER_ENCODING} eq 'chunked' }; + # If a message is received with both a Transfer-Encoding and a + # Content-Length header field, the Transfer-Encoding overrides the + # Content-Length. Such a message might indicate an attempt to + # perform request smuggling (Section 9.5) or response splitting + # (Section 9.4) and ought to be handled as an error. A sender MUST + # remove the received Content-Length field prior to forwarding such + # a message downstream. + if ($chunked && $env->{CONTENT_LENGTH}) { + last; # Return bad response. + } + if ( $env->{HTTP_EXPECT} ) { if ( lc $env->{HTTP_EXPECT} eq '100-continue' ) { $self->write_all($conn, "HTTP/1.1 100 Continue\015\012\015\012") @@ -307,6 +318,10 @@ sub handle_connection { } if (my $cl = $env->{CONTENT_LENGTH}) { + if ($cl !~ /^[0-9]+$/) { # content-length header must be digits. + last; # Return bad response + } + my $buffer = Plack::TempBuffer->new($cl); while ($cl > 0) { my $chunk;
t/15smuggling-content-length-and-transfer-encoding.t+59 −0 added@@ -0,0 +1,59 @@ +use strict; +use Test::TCP; +use Plack::Test; +use HTTP::Request; +use HTTP::Message::PSGI; +use Test::More; +use Digest::MD5; +use Plack::Test::Server; +use Test::TCP; +use IO::Socket::INET; + +$ENV{PLACK_SERVER} = 'Starlet'; + +my $app = sub { + my $env = shift; + my $body; + my $clen = $env->{CONTENT_LENGTH}; + while ($clen > 0) { + $env->{'psgi.input'}->read(my $buf, $clen) or last; + $clen -= length $buf; + $body .= $buf; + } + return [ 200, [ 'Content-Type', 'text/plain', 'X-Content-Length', $env->{CONTENT_LENGTH} ], [ $body ] ]; +}; + +my $server = Test::TCP->new( + code => sub { + my $sock_or_port = shift; + my $server = Plack::Loader->auto( + port => $sock_or_port, + host => '127.0.0.1' + ); + $server->run($app); + exit; + }, +); + +my $sock = IO::Socket::INET->new( + PeerAddr => '127.0.0.1', + PeerPort => $server->port, + Proto => 'tcp', +); + +print {$sock} ( + "GET / HTTP/1.1\015\012" + . "content-length: 3\015\012" + . "Transfer-Encoding: chunked\015\012" + . "connection: close\015\012" + . "\015\012" + . "8\015\012" + . "SMUGGLED\015\012" + . "0\015\012" +); + +my $res_str = do { local $/; <$sock> }; +my ($status_line, ) = split /\015\012/, $res_str; +is $status_line, 'HTTP/1.1 400 Bad Request'; + +done_testing;
t/16smuggling-multiple-content-length-header.t+57 −0 added@@ -0,0 +1,57 @@ +use strict; +use Test::TCP; +use Plack::Test; +use HTTP::Request; +use HTTP::Message::PSGI; +use Test::More; +use Digest::MD5; +use Plack::Test::Server; +use Test::TCP; +use IO::Socket::INET; + +$ENV{PLACK_SERVER} = 'Starlet'; + +my $app = sub { + my $env = shift; + my $body; + my $clen = $env->{CONTENT_LENGTH}; + while ($clen > 0) { + $env->{'psgi.input'}->read(my $buf, $clen) or last; + $clen -= length $buf; + $body .= $buf; + } + return [ 200, [ 'Content-Type', 'text/plain', 'X-Content-Length', $env->{CONTENT_LENGTH} ], [ $body ] ]; +}; + +my $server = Test::TCP->new( + code => sub { + my $sock_or_port = shift; + my $server = Plack::Loader->auto( + port => $sock_or_port, + host => '127.0.0.1' + ); + $server->run($app); + exit; + }, +); + +my $sock = IO::Socket::INET->new( + PeerAddr => '127.0.0.1', + PeerPort => $server->port, + Proto => 'tcp', +); + +print {$sock} ( + "GET / HTTP/1.1\015\012" + . "content-length: 3\015\012" + . "content-length: 9\015\012" + . "connection: close\015\012" + . "\015\012" + . "123456789" +); + +my $res_str = do { local $/; <$sock> }; +my ($status_line, ) = split /\015\012/, $res_str; +is $status_line, 'HTTP/1.1 400 Bad Request'; + +done_testing;
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
4- github.com/kazuho/Starlet/commit/a7d5dfd1862aafa43e5eaca0fdb6acf4cc15b2d0.patchnvdPatch
- www.openwall.com/lists/oss-security/2026/05/03/1nvdMailing ListThird Party Advisory
- datatracker.ietf.org/doc/html/rfc7230nvdThird Party Advisory
- metacpan.org/release/KAZUHO/Starlet-0.32/changesnvd
News mentions
0No linked articles in our index yet.