Skip to content

Commit

Permalink
fix Windows path traversal bug (closes #738)
Browse files Browse the repository at this point in the history
  • Loading branch information
kraih committed Feb 2, 2015
1 parent 0cc15ac commit 9ffa38f
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Changes
@@ -1,5 +1,7 @@

5.76 2015-02-02
- Emergency release for a critical security issue that can expose files on
Windows systems, everybody should update!
- Increased default max_message_size from 10MB to 16MB in Mojo::Message.
- Reduced default max_line_size from 10KB to 8KB in Mojo::Headers and
Mojo::Message.
Expand Down
12 changes: 10 additions & 2 deletions lib/Mojo/Path.pm
Expand Up @@ -15,7 +15,7 @@ sub canonicalize {

my $parts = $self->parts;
for (my $i = 0; $i <= $#$parts;) {
if ($parts->[$i] eq '.' || $parts->[$i] eq '') { splice @$parts, $i, 1 }
if ($parts->[$i] =~ /^(?:\.||\.{3,})$/) { splice @$parts, $i, 1 }
elsif ($i < 1 || $parts->[$i] ne '..' || $parts->[$i - 1] eq '..') { $i++ }
else { splice @$parts, --$i, 2 }
}
Expand Down Expand Up @@ -169,14 +169,22 @@ following new ones.
$path = $path->canonicalize;
Canonicalize path.
Canonicalize path, parts comprised solely of three or more dots are treated as
a single dot, to protect certain operating systems from path traversal
attacks.
# "/foo/baz"
Mojo::Path->new('/foo/./bar/../baz')->canonicalize;
# "/../baz"
Mojo::Path->new('/foo/../bar/../../baz')->canonicalize;
# "/foo/bar"
Mojo::Path->new('/foo/.../bar')->canonicalize;
# "/foo/bar"
Mojo::Path->new('/foo/...../bar')->canonicalize;
=head2 clone
my $clone = $path->clone;
Expand Down
2 changes: 1 addition & 1 deletion lib/Mojolicious/Static.pm
Expand Up @@ -32,7 +32,7 @@ sub dispatch {
$path = $stash->{path} ? $path->new($stash->{path}) : $path->clone;
return undef unless my @parts = @{$path->canonicalize->parts};

# Serve static file and prevent directory traversal
# Serve static file and prevent path traversal
return undef if $parts[0] eq '..' || !$self->serve($c, join('/', @parts));
$stash->{'mojo.static'} = 1;
return !!$c->rendered;
Expand Down
18 changes: 18 additions & 0 deletions t/mojo/path.t
Expand Up @@ -88,6 +88,24 @@ is_deeply $path->parts, [qw(.. .. .. .. .. .. .. .. etc passwd)],
ok $path->leading_slash, 'has leading slash';
ok !$path->trailing_slash, 'no trailing slash';

# Canonicalize (triple dot)
$path = Mojo::Path->new('/foo/.../.../windows/win.ini');
is "$path", '/foo/.../.../windows/win.ini', 'same path';
is_deeply $path->parts, [qw(foo ... ... windows win.ini)], 'right structure';
is $path->canonicalize, '/foo/windows/win.ini', 'canonicalized path';
is_deeply $path->parts, [qw(foo windows win.ini)], 'right structure';
ok $path->leading_slash, 'has leading slash';
ok !$path->trailing_slash, 'no trailing slash';

# Canonicalize (more dots)
$path = Mojo::Path->new('/foo/....../windows/win.ini');
is "$path", '/foo/....../windows/win.ini', 'same path';
is_deeply $path->parts, [qw(foo ...... windows win.ini)], 'right structure';
is $path->canonicalize, '/foo/windows/win.ini', 'canonicalized path';
is_deeply $path->parts, [qw(foo windows win.ini)], 'right structure';
ok $path->leading_slash, 'has leading slash';
ok !$path->trailing_slash, 'no trailing slash';

# Canonicalizing (with escaped "%")
$path = Mojo::Path->new('%2ftest%2f..%252f..%2f..%2f..%2f..%2fetc%2fpasswd');
is "$path", '%2ftest%2f..%252f..%2f..%2f..%2f..%2fetc%2fpasswd', 'same path';
Expand Down
6 changes: 6 additions & 0 deletions t/mojolicious/app.t
Expand Up @@ -392,6 +392,12 @@ $t->get_ok('/../../mojolicious/secret.txt')->status_is(404)
->header_is(Server => 'Mojolicious (Perl)')
->content_like(qr/Page not found/);

# Try to access a file which is not under the web root via path
# traversal (triple dot)
$t->get_ok('/.../mojolicious/secret.txt')->status_is(404)
->header_is(Server => 'Mojolicious (Perl)')
->content_like(qr/Page not found/);

# Check If-Modified-Since
$t->get_ok('/hello.txt' => {'If-Modified-Since' => $mtime})->status_is(304)
->header_is(Server => 'Mojolicious (Perl)')->content_is('');
Expand Down
6 changes: 6 additions & 0 deletions t/mojolicious/production_app.t
Expand Up @@ -100,6 +100,12 @@ $t->get_ok('/../../mojolicious/secret.txt')->status_is(404)
->header_is(Server => 'Mojolicious (Perl)')
->content_like(qr/Page not found/);

# Try to access a file which is not under the web root via path
# traversal in production mode (triple dot)
$t->get_ok('/.../mojolicious/secret.txt')->status_is(404)
->header_is(Server => 'Mojolicious (Perl)')
->content_like(qr/Page not found/);

# Embedded production static file
$t->get_ok('/some/static/file.txt')->status_is(200)
->header_is(Server => 'Mojolicious (Perl)')
Expand Down
6 changes: 6 additions & 0 deletions t/mojolicious/testing_app.t
Expand Up @@ -45,4 +45,10 @@ $t->get_ok('/../../mojolicious/secret.txt')->status_is(404)
->header_is(Server => 'Mojolicious (Perl)')
->content_like(qr/Testing not found/);

# Try to access a file which is not under the web root via path
# traversal in testing mode (triple dot)
$t->get_ok('/.../mojolicious/secret.txt')->status_is(404)
->header_is(Server => 'Mojolicious (Perl)')
->content_like(qr/Testing not found/);

done_testing();

0 comments on commit 9ffa38f

Please sign in to comment.