potentially dangerous behaviour in CGI_lite.pm file upload

Andrew McNaughton (andrew@SQUIZ.CO.NZ)
Tue, 12 Aug 1997 20:47:30 +1200

In recent versions of CGI_lite.pm, files uploaded via multipart/form-data
MIME encoding is saved using a filename which may be unsafe (ie contain
shell commands). While CGI_lite's operations on these files are apparently
safe, file upload scripts based on CGI_lite may not be.

This problem has been tested and confirmed with version 1.7, and it also
appears to affect versions 1.62 and 1.8.

If you don't use it, You might want to disable the upload system altogether
in this module. ie comment out lines as follows in the parse_form_data()
routine

if (!$content_type ||
($content_type eq 'application/x-www-form-urlencoded')) {

local $^W = 0;

read (STDIN, $post_data, $content_length);
$self->_decode_url_encoded_data (\$post_data, 'form');

return wantarray ?
%{ $self->{web_data} } : $self->{web_data};

# } elsif ($content_type =~ /multipart\/form-data/) {
# ($boundary) = $content_type =~ /boundary=(\S+)$/;
# $self->_parse_multipart_data ($content_length, $boundary);
#
# return wantarray ?
# %{ $self->{web_data} } : $self->{web_data};
} else {
$self->_error ('Invalid content type!');
}

This might seem a bit heavy handed, but I think there's another bug in
there which I have not yet been able to replicate. While testing, I wound
up with a zombie process which created new files once a second until
killed. I can't guarantee that this was not from changes I made to
CGI_lite.pm, but I don't think so. I was mostly just putting in a few
print statements to see what was going on.

If you do use the file upload system, you should properly escape the file
names immediately before opening filehandles. Your programs probably
expect url escaping, so this is probably the best format to use. See
URI::Escape.pm in libwww for code to do this. Make sure all shell
metacharacters are escaped, not just the url-unsafe ones. Don't forget \n.

--------------------------------------------------------------------------------

DETAILS:
========

Netscape (the only browser implementing file upload that I have access to)
url-encodes filenames before sending them, but it is relatively
straightforward to manually construct a http request which does not do this
url-encoding.

----------------- start ---------------------
POST /cgi-bin/test/upload.pl HTTP/1.0
Referer: file:///Hard_Disk/pub/projects/Utilities/w3mir/upload.html
Connection: Keep-Alive
User-Agent: Mozilla/3.01 (Macintosh; I; PPC)
Host: 127.0.0.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Accept-Language: en
Content-type: multipart/form-data;
boundary=---------------------------29741200957742
Content-Length: 186

-----------------------------29741200957742
Content-Disposition: form-data; name="readme"; filename=";echo foo>foo|"

file contents
-----------------------------29741200957742--

---------------------- end -------------------

the script will create a file whose name consists of a numerical timestamp
followed by two underscores and then the user nominated filename.

CGI_lite's handling of these files is apparently safe. Scripts which make
use of the filenames recieved from CGI_lite.pm may not be.

$filename = ";cat /etc/passwd>foo|";

open (FILE, ">$filename"); # safe
open (FILE, "<$filename"); # safe
open (FILE, ">" . $filename); # safe
open (FILE, "<" . $filename); # safe
open (FILE, "$filename"); # not safe
open (FILE, $filename); # not safe

If the last is run by a CGI sript, the contents of /etc/passwd will be
written to STDOUT, and thereby the web client (more likely telnet than
netscape in the event of an exploit).

There are two ways for scripts to access the uploaded file(s).

---------------------- start -------------------
#!/usr/local/bin/perl5
use CGI_Lite;
$cgi = new CGI_Lite ();
$cgi->set_directory ("/usr/foo/tmp")
|| die "Directory doesn't exist.\n";
$cgi->set_platform ("UNIX");
$cgi->set_file_type ("handle"); # return handles.
$data = $cgi->parse_form_data ();

print "Content-type: text/plain", "\n\n";

$filename = $$data{'readme'};
while (<$filename>) {
print;
}

close ($filename);
exit (0);

---------------------- end -------------------

Safe so far, although subsequent uses of these files might be dangerously
implemented.

Actually CGI_lite.pm version 1.7 is broken such that using file handles
will only work if the upload directory directory is set to "" which results
in files being stored in the current directory.

---------------------- start -------------------
#!/usr/local/bin/perl5
use CGI_Lite;
$cgi = new CGI_Lite ();
$cgi->set_directory ("/usr/shishir")
|| die "Directory doesn't exist.\n";
$cgi->set_platform ("UNIX");
$cgi->set_file_type ("name"); # return filenames. default behaviour
$data = $cgi->parse_form_data ();

print "Content-type: text/plain", "\n\n";

$filename = $$data{'readme'};
open (FILE , $filename);
while (<FILE>) {
print;
}

close ($filename);
exit (0);

---------------------- end -------------------

this will execute the command embedded in the filename.

There is no problem if the file is opened like so:

open (FILE , "<$filename");

In this case the pipe on the end of filename does not result in commands in
filename being passed to the shell, being overriden by the '<'.

I'd reccomend that the preceding > and < characters on the filename should
be used when opening files in perl unless you're absolutely sure the
filename is safe (and probably then also).

Check out the newsroom: http://www.newsroom.co.nz
. . . . . . . . . . . . . . . .
Andrew McNaughton | I tried to make it idiot proof,
Andrew@squiz.co.nz | but they just developed a
http://www.squiz.co.nz | better idiot

. . . . . . . . . . . . . . . .