Tweet

Writing a CGI upload form

In this tutorial you will learn how to write a perl CGI form that allows the uploading of files from a user's PC to the server on which your CGI script is running. If you have never done any perl CGI programming before, we suggest you first look at the CGI Tutorial and CGI Forms Tutorial.

Firstly you need to ensure that you have access to a web server (that has perl installed), that it has cgi enabled, and that you can put your perl script into the cgi directory.

Starting your script

Like a standard perl CGI form we use the CGI module to output the HTML. We take advantage of the CGI module's POST_MAX variable to set an upper limit on the size of files that we will allow to be uploaded. We also turn on taint checking with the -T switch.

    #!/usr/bin/perl -T

    use strict;
    use warnings;
    use CGI;
    use CGI::Carp qw(fatalsToBrowser);     # Remove for production use

    $CGI::POST_MAX = 1024 * 100;  # maximum upload filesize is 100K

    my $q = new CGI;

    print $q->header;

    print $q->start_html(-title => 'An example file upload web page');

Starting the form

Because we are uploading a file we need a multipart form rather than a standard form:

    print $q->start_multipart_form;

Or alternately:

    print $q->start_form(
        -enctype => &CGI::MULTIPART,
    );

The upload field

Using the object oriented CGI methods, you can output a form upload field like this:

    print $q->filefield(
        -name => 'filename',
    );

While you can specify a default value for the filename to be uploaded, at present no browsers will honour it, as it's considered a possible security problem. In the same respect you can't alter the value of the field from JavaScript.

Let's add a submit button for a complete form:

    print $q->submit(-value => 'Upload File');
    print $q->end_form;

Uploading the file

Later on in your code that handles the submitted form values, you need to upload the file. Here's a code fragment that uploads the user's file in 1024 byte chunks, using the read() function. Note also that we use the three argument form of open() which lets us specify the file open mode ('>'), separately and ensures that any characters that are special to the shell (such as '>' and '|') appearing in the filename argument, are treated literally.

    my $filename = $q->param('filename');
    my $output_file = "/tmp/uploaded";
    my ($bytesread, $buffer);
    my $numbytes = 1024;

    open (OUTFILE, ">", "$output_file") 
        or die "Couldn't open $output_file for writing: $!";
    while ($bytesread = read($filename, $buffer, $numbytes)) {
        print OUTFILE $buffer;
    }
    close OUTFILE;

Working example

Realistically you'll want more functionality than we've shown so far. For example, you probably don't always want to write the file to "/tmp/uploaded" and you're advised to use taint checking to ensure that the filename supplied by the user matches a regular expression of allowable characters. Your notion of what is allowable or safe will depend on your circumstances and also on the web server operating system. The following example (which assumes a Unix like web server operating system), is a working example of a cgi script that will upload files to the /tmp directory.

    #!/usr/bin/perl -T
    # A sample file upload script
    # www.perlmeme.org

    use strict;
    use warnings;
    use CGI;
    use CGI::Carp qw(fatalsToBrowser);    # Remove for production use

    $CGI::POST_MAX = 1024 * 100;  # maximum upload filesize is 100K

    sub save_file($);

    #
    # Build the form
    #

    my $q = new CGI;

    print $q->header;
    print $q->start_html(
        -title => "An example file upload web page",
    );
    print $q->h3('Use this form to upload a local file to the web server'),
          $q->start_multipart_form(
              -name    => 'main_form');
    print 'Enter a filename, or click on the browse button to choose one: ',
          $q->filefield(
              -name      => 'filename',
    	  -size      => 40,
    	  -maxlength => 80);
    print $q->hr;
    print $q->submit(-value => 'Upload the file');
    print $q->hr;
    print $q->end_form;

    #
    # Look for uploads that exceed $CGI::POST_MAX
    #

    if (!$q->param('filename') && $q->cgi_error()) {
        print $q->cgi_error();
        print <<'EOT';
    <p>
    The file you are attempting to upload exceeds the maximum allowable file size.
    <p>
    Please refer to your system administrator
    EOT
        print $q->hr, $q->end_html;
        exit 0;
    }

    #
    # Upload the file
    #

    if ($q->param()) {
        save_file($q);
    }

    print $q->end_html;
    exit 0;

    #-------------------------------------------------------------

    sub save_file($) {

        my ($q) = @_;
        my ($bytesread, $buffer);
        my $num_bytes = 1024;
        my $totalbytes;
        my $filename = $q->upload('filename');
        my $untainted_filename;

        if (!$filename) {
            print $q->p('You must enter a filename before you can upload it');
    	return;
        }

        # Untaint $filename

        if ($filename =~ /^([-\@:\/\\\w.]+)$/) {
            $untainted_filename = $1;
        } else {
            die <<"EOT";
    Unsupported characters in the filename "$filename". 
    Your filename may only contain alphabetic characters and numbers, 
    and the characters '_', '-', '\@', '/', '\\' and '.'
    EOT
        }

        if ($untainted_filename =~ m/\.\./) {
            die <<"EOT";
    Your upload filename may not contain the sequence '..' 
    Rename your file so that it does not include the sequence '..', and try again.
    EOT
        }

        my $file = "/tmp/$untainted_filename";

        print "Uploading $filename to $file<BR>";

        # If running this on a non-Unix/non-Linux/non-MacOS platform, be sure to 
        # set binmode on the OUTFILE filehandle, refer to 
        #    perldoc -f open 
        # and
        #    perldoc -f binmode

        open (OUTFILE, ">", "$file") or die "Couldn't open $file for writing: $!";

        while ($bytesread = read($filename, $buffer, $num_bytes)) {
            $totalbytes += $bytesread;
            print OUTFILE $buffer;
        }
        die "Read failure" unless defined($bytesread);
        unless (defined($totalbytes)) {
            print "<p>Error: Could not read file ${untainted_filename}, ";
            print "or the file was zero length.";
        } else {
            print "<p>Done. File $filename uploaded to $file ($totalbytes bytes)";
        }
        close OUTFILE or die "Couldn't close $file: $!";

    }
    #-------------------------------------------------------------

See also

    perldoc CGI
    perldoc -f open
    perldoc perlsec
Revision: 1.4 [Top]