Skip to content

Commit faebad5

Browse files
Theo van Hoeseloalders
authored andcommitted
Add new test for Content-Length issues
prove we fixed CVE-2022-31081
1 parent 8dc5269 commit faebad5

1 file changed

Lines changed: 278 additions & 0 deletions

File tree

t/content_length.t

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
use strict;
2+
use warnings;
3+
4+
use Test::More 0.98;
5+
6+
use Config;
7+
8+
use HTTP::Daemon;
9+
use HTTP::Response;
10+
use HTTP::Status;
11+
use HTTP::Tiny 0.042;
12+
13+
patch_http_tiny(); # do not fix Content-Length, we want to forge something bad
14+
15+
plan skip_all => "This system cannot fork" unless can_fork();
16+
17+
my $BASE_URL;
18+
my @TESTS = get_tests();
19+
20+
for my $test (@TESTS) {
21+
22+
my $http_daemon = HTTP::Daemon->new() or die "HTTP::Daemon->new: $!";
23+
$BASE_URL = $http_daemon->url;
24+
25+
my $pid = fork;
26+
die "fork: $!" if !defined $pid;
27+
if ($pid == 0) {
28+
accept_requests($http_daemon);
29+
}
30+
31+
my $resp = http_test_request($test);
32+
33+
ok $resp, $test->{title};
34+
35+
is $resp->{status}, $test->{status},
36+
"... and has expected status";
37+
38+
like $resp->{content}, $test->{like},
39+
"... and body does match"
40+
if $test->{like};
41+
42+
}
43+
44+
done_testing;
45+
46+
47+
48+
sub get_tests{
49+
{
50+
title => "Hello World Request ... it works as expected",
51+
path => "hello-world",
52+
status => 200,
53+
like => qr/^Hello World$/,
54+
},
55+
{
56+
title => "Positive Content Length",
57+
method => "POST",
58+
headers => {
59+
'Content-Length' => '+1', # quotes are needed to retain plus-sign
60+
},
61+
status => 400,
62+
like => qr/value must be a unsigned integer/,
63+
},
64+
{
65+
title => "Negative Content Length",
66+
method => "POST",
67+
headers => {
68+
'Content-Length' => '-1',
69+
},
70+
status => 400,
71+
like => qr/value must be a unsigned integer/,
72+
},
73+
{
74+
title => "Non Integer Content Length",
75+
method => "POST",
76+
headers => {
77+
'Content-Length' => '3.14',
78+
},
79+
status => 400,
80+
like => qr/value must be a unsigned integer/,
81+
},
82+
{
83+
title => "Explicit Content Length ... with exact length",
84+
method => "POST",
85+
headers => {
86+
'Content-Length' => '8',
87+
},
88+
body => "ABCDEFGH",
89+
status => 200,
90+
like => qr/^ABCDEFGH$/,
91+
},
92+
{
93+
title => "Implicit Content Length ... will always pass",
94+
method => "POST",
95+
body => "ABCDEFGH",
96+
status => 200,
97+
like => qr/^ABCDEFGH$/,
98+
},
99+
{
100+
title => "Shorter Content Length ... gets truncated",
101+
method => "POST",
102+
headers => {
103+
'Content-Length' => '4',
104+
},
105+
body => "ABCDEFGH",
106+
status => 200,
107+
like => qr/^ABCD$/,
108+
},
109+
{
110+
title => "Different Content Length ... must fail",
111+
method => "POST",
112+
headers => {
113+
'Content-Length' => ['8', '4'],
114+
},
115+
body => "ABCDEFGH",
116+
status => 400,
117+
like => qr/values are not the same/,
118+
},
119+
{
120+
title => "Underscore Content Length ... must match",
121+
method => "POST",
122+
headers => {
123+
'Content_Length' => '4',
124+
},
125+
body => "ABCDEFGH",
126+
status => 400,
127+
like => qr/values are not the same/,
128+
},
129+
{
130+
title => "Longer Content Length ... gets timeout",
131+
method => "POST",
132+
headers => {
133+
'Content-Length' => '9',
134+
},
135+
body => "ABCDEFGH",
136+
status => 599, # silly code !!!
137+
like => qr/^Timeout/,
138+
},
139+
140+
}
141+
142+
143+
144+
sub router_table {
145+
{
146+
'/hello-world' => {
147+
'GET' => sub {
148+
my $resp = HTTP::Response->new(200);
149+
$resp->content('Hello World');
150+
return $resp;
151+
},
152+
},
153+
154+
'/' => {
155+
'POST' => sub {
156+
my $rqst = shift;
157+
158+
my $body = $rqst->content();
159+
160+
my $resp = HTTP::Response->new(200);
161+
$resp->content($body);
162+
163+
return $resp
164+
},
165+
},
166+
}
167+
}
168+
169+
170+
171+
sub can_fork {
172+
$Config{d_fork} || (($^O eq 'MSWin32' || $^O eq 'NetWare')
173+
and $Config{useithreads}
174+
and $Config{ccflags} =~ /-DPERL_IMPLICIT_SYS/);
175+
}
176+
177+
178+
179+
# run the mini HTTP dispatcher that can handle various routes / methods
180+
sub accept_requests{
181+
my $http_daemon = shift;
182+
while (my $conn = $http_daemon->accept) {
183+
while (my $rqst = $conn->get_request) {
184+
if (my $resp = dispatch_request($rqst)) {
185+
$conn->send_response($resp);
186+
}
187+
}
188+
$conn->close;
189+
undef($conn);
190+
$http_daemon->close;
191+
exit 1;
192+
}
193+
}
194+
195+
196+
197+
sub dispatch_request{
198+
my $rqst = shift
199+
or return;
200+
my $path = $rqst->uri->path
201+
or return;
202+
my $meth = $rqst->method
203+
or return;
204+
my $code = router_table()->{$path}{$meth}
205+
or return HTTP::Response->new(RC_NOT_FOUND);
206+
my $resp = $code->($rqst);
207+
return $resp;
208+
}
209+
210+
211+
212+
sub http_test_request {
213+
my $test = shift;
214+
my $http_client = HTTP::Tiny->new(
215+
timeout => 5,
216+
proxy => undef,
217+
http_proxy => undef,
218+
https_proxy => undef,
219+
);
220+
my $resp;
221+
eval {
222+
local $SIG{ALRM} = sub { die "Timeout\n" };
223+
alarm 2;
224+
$resp = $http_client->request(
225+
$test->{method} || "GET",
226+
$BASE_URL . ($test->{path} || ""),
227+
{
228+
headers => $test->{headers},
229+
content => $test->{body}
230+
},
231+
);
232+
};
233+
my $err = $@;
234+
alarm 0;
235+
diag $err if $err;
236+
237+
return $resp
238+
}
239+
240+
241+
242+
sub patch_http_tiny {
243+
244+
# we need to patch write_content_body
245+
# this is part of HTTP::Tiny internal module HTTP::Tiny::Handle
246+
#
247+
# the below code is from the original HTTP::Tiny module, where just two lines
248+
# have been commented out
249+
250+
no strict 'refs';
251+
252+
*HTTP::Tiny::Handle::write_content_body = sub {
253+
@_ == 2 || die(q/Usage: $handle->write_content_body(request)/ . "\n");
254+
my ($self, $request) = @_;
255+
256+
my ($len, $content_length) = (0, $request->{headers}{'content-length'});
257+
while () {
258+
my $data = $request->{cb}->();
259+
260+
defined $data && length $data
261+
or last;
262+
263+
if ( $] ge '5.008' ) {
264+
utf8::downgrade($data, 1)
265+
or die(qq/Wide character in write_content()\n/);
266+
}
267+
268+
$len += $self->write($data);
269+
}
270+
271+
# this should not be checked during our tests, we want to forge bad requests
272+
#
273+
# $len == $content_length
274+
# or die(qq/Content-Length mismatch (got: $len expected: $content_length)\n/);
275+
276+
return $len;
277+
};
278+
}

0 commit comments

Comments
 (0)