VYPR
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

2
  • Kazuho/Starletreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • cpe:2.3:a:kazuho:starlet:*:*:*:*:*:perl:*:*range: <=0.31

Patches

1
a7d5dfd1862a

Prevent HTTP Smuggling.

https://github.com/kazuho/StarletTokuhiro MatsunoNov 19, 2019via nvd-ref
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

News mentions

0

No linked articles in our index yet.