awohld has asked for the wisdom of the Perl Monks concerning the following question:

I need to make my code immune from SQL injection attacks and any other vulnerabilities. I tried to filter out any malicious characters where people could do harm but this is my first script I made myself and I'm not sure if it's safe. Can I get any recommendations on making it hacker proof?
#!/usr/bin/perl use DBI; use CGI; #use CGI::Carp qw(fatalsToBrowser); read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'}); @pairs = split(/&/, $buffer); foreach $pair (@pairs) { ($name, $value) = split(/=/, $pair); $value =~ tr/+/ /; $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; $value =~ s/\;|\<|\>|\?|\n|\f|\r|\\|\|//g; #Clean harmful + characters. $value =~ s/'/\\'/g; #replace all ' with /' $value =~ s/"/\\"/g; #replace all " with /" if ($INPUT{$name}) { $INPUT{$name} = $INPUT{$name}."," +.$value; } else { $INPUT{$name} = $value; } $value =~ s/<!--(.|\n)*-->//g; } ###########################Begin Get Date############################# +########## #Defines arrays for the day of the week and month of the year. @days = ('Sunday','Monday','Tuesday','Wednesday', 'Thursday','Friday','Saturday'); @months = ('January','February','March','April','May','June','July +', 'August','September','October','November','December') +; # Get the current time and format the hour, minutes and seconds. +Add # 1900 to the year to get the full 4 digit year. ($sec,$min,$hour,$mday,$mon,$year,$wday) = (localtime(time))[0,1,2 +,3,4,5,6]; $time = sprintf("%02d:%02d:%02d",$hour,$min,$sec); $year += 1900; # Format the date. $date = "$days[$wday], $months[$mon] $mday, $year at $time"; #print "$days[$wday], $months[$mon] $mday, $year at $time"; ###################################################################### +########## ##Set Technology###################################################### +#### # If technology type isn't selected, it needs to be set to false. if (!$INPUT{'radar'}) {$INPUT{'radar'} = "false"}; if (!$INPUT{'laser'}) {$INPUT{'laser'} = "false"}; if (!$INPUT{'vascar'}) {$INPUT{'vascar'} = "false"}; if (!$INPUT{'airplane'}) {$INPUT{'airplane'} = "false"}; if (!$INPUT{'photo'}) {$INPUT{'photo'} = "false"}; if (!$INPUT{'roadblock'}) {$INPUT{'roadblock'} = "false"}; if (!$INPUT{'unknown'}) {$INPUT{'unknown'} = "false"}; ###################################################################### +########## ##Start database connections########################################## +########## $database = "database"; $db_server = "localhost"; $user = "user"; $password = "password"; ##Connect to database, insert statement, & disconnect ################ +########## $dbh = DBI->connect("DBI:mysql:$database:$db_server", $user, $password +); $statement = "INSERT INTO speedtrap (state, city, locationname, refere +ncename, lat_deg, lat_min, lat_sec, long_deg, long_min, long_sec, xco +ord, ycoord, ttime, level, radar, laser, vascar, airplane, photo, roa +dblock, unknown, comments, email, name, date_added) VALUES ('".$INPUT +{'state'} ."', '".$INPUT{'city'} ."','".$INPUT{'locationname'} . "',' +".$INPUT{'referencename'} ."','".$INPUT{'lat_deg'} ."','".$INPUT{'lat +_min'} ."','".$INPUT{'lat_sec'} ."','".$INPUT{'long_deg'} ."', '".$IN +PUT{'long_min'} ."', '".$INPUT{'long_sec'} ."','".$INPUT{'xcoord'} ." +','".$INPUT{'ycoord'} ."','".$INPUT{'ttime'} ."', '".$INPUT{'level'} +."', '".$INPUT{'radar'} ."','".$INPUT{'laser'} ."', '".$INPUT{'vascar +'} ."', '".$INPUT{'airplane'} ."','".$INPUT{'photo'} ."', '".$INPUT{' +roadblock'} ."', '".$INPUT{'unknown'} ."', '".$INPUT{'comments'} ."', +'".$INPUT{'email'} ."', '".$INPUT{'name'} . "','" . $date."')"; $sth = $dbh->prepare($statement) or die "Couldn't prepare the query: $ +sth->errstr"; $rv = $sth->execute or die "Couldn't execute query: $dbh->errstr"; $rc = $sth->finish; $rc = $dbh->disconnect; ###################################################################### +########## $query = new CGI; print $query->redirect('http://www.yourname.com/index.html');

Replies are listed 'Best First'.
Re: Hacker Proofing My First Script
by tachyon (Chancellor) on Sep 30, 2004 at 07:38 UTC

    The *most* important missing element is using placeholders in the SQL ie 'INSERT INTO foo VALUES(?,?)'. As your code stands it is totally open to injection as I can pass any values and you don't check those values and you interpolate them directly. Your code is pretty much as insecure as it is possible to be. Placeholders will do the quoting and fix this. I recommend A short guide to DBI. You should also read Ovids CGI tutorial. This is how you might well write it in a few weeks/months....

    #!/usr/bin/perl -wT use strict; use DBI; use CGI; use POSIX 'strftime'; my $database = "database"; my $db_server = "localhost"; my $user = "user"; my $password = "password"; my $redirect = 'http://www.yourname.com/index.html'; my @fields = qw( state city locationname referencename lat_de +g lat_min lat_sec long_deg long_min long_s +ec xcoord ycoord ttime level radar laser vascar airplane photo roadbl +ock unknown comments email name date_a +dded ); my $q = CGI->new(); my %INPUT = $q->Vars; $INPUT{$_} ||= '' for @fields; $INPUT{$_} ||= 'false' for qw( radar laser vascar airplane photo roadb +lock unknown ); $INPUT{'date_added'} = strftime("%A, %B %d, %Y at %H:%M:%S", localtime +() ); my $dbh = DBI->connect("DBI:mysql:$database:$db_server", $user, $passw +ord); my $sql = 'INSERT INTO speedtrap (' . ( join ',', @fields ) . ') ' . 'VALUES (' . ( join ',', ('?') x scalar(@fields) ) . ')'; $dbh->do( $sql, @INPUT{@fields} ); $dbh->disconnect; print $q->redirect($redirect);

    I hope this shows you a few things. I would however suggest you store epoch time ie the 32 bit integer you get from time() in your database. It makes extracting data between two dates much easier and takes up much less room. If is trivial to format it into a nice string for output. You should validate data before you stuff it into your database or you will accumulate rubbish, better it never gets there in the first place.

    cheers

    tachyon

      I've updated my script, how does this look? Did I use the DBI placeholder correctly, will I be immune from SQL injection attacks?

      Thanks for everyones help, I couldn't of done it without your support.

      #!/usr/bin/perl -wT use DBI; use strict; use CGI; use POSIX 'strftime'; my $q = CGI->new(); my $sth; my $state = $q->param ('state'); my $city = $q->param ('city'); my $locationname = $q->param ('locationname'); my $referencename = $q->param ('referencename'); my $lat_deg = $q->param ('lat_deg'); my $lat_min = $q->param ('lat_min'); my $lat_sec = $q->param ('lat_sec'); my $long_deg = $q->param ('long_deg'); my $long_min = $q->param ('long_min'); my $long_sec = $q->param ('long_sec'); my $xcoord = $q->param ('xcoord'); my $ycoord = $q->param ('ycoord'); my $ttime = $q->param ('ttime'); my $level = $q->param ('level'); my $radar = $q->param ('radar'); my $laser = $q->param ('laser'); my $vascar = $q->param ('vascar'); my $airplane = $q->param ('airplane'); my $photo = $q->param ('photo'); my $roadblock = $q->param ('roadblock'); my $redlight = $q->param ('redlight'); my $unknown = $q->param ('unknown'); my $comments = $q->param ('comments'); my $email = $q->param ('email'); my $name = $q->param ('name'); my $date = strftime("%A, %B %d, %Y at %H:%M:%S", localtime() ); ##Set Radar Technology################################################ +########## # If technology type isn't selected, it needs to be set to false. if (!$q->param ('radar')) {$radar = "false"}; if (!$q->param ('laser')) {$laser = "false"}; if (!$q->param ('vascar')) {$vascar = "false"}; if (!$q->param ('airplane')) {$airplane = "false"}; if (!$q->param ('photo')) {$photo = "false"}; if (!$q->param ('roadblock')) {$roadblock = "false"}; if (!$q->param ('redlight')) {$redlight = "false"}; if (!$q->param ('unknown')) {$unknown = "false"}; ###################################################################### +########## ##Start database connections########################################## +########## my $database = "database"; my $db_server = "localhost"; my $user = "user"; my $password = "password"; ##Connect to database, insert statement, & disconnect ################ +########## my $dbh = DBI->connect("DBI:mysql:$database:$db_server", $user, $passw +ord); my $statement = "INSERT INTO trap1 (state, city, locationname, referen +cename, lat_deg, lat_min, lat_sec, long_deg, long_min, long_sec, xcoo +rd, ycoord, ttime, level, radar, laser, vascar, airplane, photo, road +block, redlight, unknown, comments, email, name, date_added) VALUES ( +?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; $sth = $dbh->prepare($statement) or die "Couldn't prepare the query +: $sth->errstr"; my $rv = $sth->execute($state,$city,$locationname,$referencename,$lat_ +deg,$lat_min,$lat_sec,$long_deg,$long_min,$long_sec,$xcoord,$ycoord,$ +ttime,$level,$radar,$laser,$vascar,$airplane,$photo,$roadblock,$redli +ght,$unknown,$comments,$email,$name,$date) or die "Couldn't execute q +uery: $dbh->errstr"; my $rc = $sth->finish; $rc = $dbh->disconnect; ###################################################################### +########## print "Content-type: text/html\n\n"; print $q->redirect('http://www.site.com/index.html');

        Yes that is secure. Still a bit ugly, but secure nonethless. Why declare $sth at the top, miles from where you use it? Why collect the return codes in $rc - you don't *do* anything with them? It is much more convenient to put constants like your DB passwd etc at the top so you can find them.

        You have a bug. The $q->redirect header will never be acted upon as you terminate the headers with your Content-Type header. You don't need that header, just the redirect. Also print $q->header() will give you a valid header so if you are using CGI why not use it. <code>

        cheers

        tachyon

Re: Hacker Proofing My First Script
by jZed (Prior) on Sep 30, 2004 at 04:24 UTC
    Limiting myself to the first four problems that jumped out at me: 1) you don't use warnings 2) you don't use strict 3) you don't use CGI.pm to read the parametrs 4) you don't use placeholders in your DBI statements (and that, is really really important when it comes to SQL injection).

    I'd advise you to do some reading (there are many good tutorials here, search for security or CGI or DBI).

    update : added link around the word tutorials, thanks PodMaster :-)

Re: Hacker Proofing My First Script
by kiat (Vicar) on Sep 30, 2004 at 06:52 UTC
    Hi awohld;
    # Your code use CGI; read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'}); @pairs = split(/&/, $buffer); foreach $pair (@pairs) { ($name, $value) = split(/=/, $pair); $value =~ tr/+/ /; $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; }
    You're importing "use CGI" but making very little use of it. There's no need for the code above. It can become messy. To get to the individual values from the submission, you can do this:

    my $comments = $q->param('comments');
    Someone here (I can't remember who) showed me a way to store the submitted values to a hash:
    my %params = map { $_ => $q->param($_) } $q->param; # comments stored in $params{'comments'}
    Thanks to davido!

      You are absolutely correct that if the OP is planning on parsing CGI form input, a much safer and more reliable way to do so (instead of inventing ones own possibly flawed solution) is to use the CGI module. ...however...

      Someone here (I can't remember who) showed me a way to store the submitted values to a hash:

      my %params = map { $_ => $q->param($_) } $q->param;

      Someone's going to be glad you couldn't remember who (s)he was. The POD for CGI is mandatory reading material if you plan to use the CGI module:

      FETCHING THE PARAMETER LIST AS A HASH:

      $params = $q->Vars; print $params->{'address'}; @foo = split("\0",$params->{'foo'}); %params = $q->Vars; use CGI ':cgi-lib'; $params = Vars;

      Many people want to fetch the entire parameter list as a hash in which the keys are the names of the CGI parameters, and the values are the parameters' values. The Vars() method does this. Called in a scalar context, it returns the parameter list as a tied hash reference. Changing a key changes the value of the parameter in the underlying CGI parameter list. Called in a list context, it returns the parameter list as an ordinary hash. This allows you to read the contents of the parameter list, but not to change it.

      The snippet in the POD shows four or five methods. I prefer the $href = $params->Vars(); most of the time, since it doesn't pass around a copy.


      Dave

Re: Hacker Proofing My First Script
by Anonymous Monk on Sep 30, 2004 at 12:09 UTC
  • Use CGI's param method to do the parsing of input. Your code fails on a GET method.
  • Define a limit on how much data you are willing to accept, or the CGI will happely eat as much data as a client is willing to feed you.
  • Use DBI place holders. This will take care of quoting, and (baring unrevealed bugs in the DBI) eliminate all possible SQL injection hacks.
  • Don't use "false" values in your database to signal anything special. That's what NULL values are for. (Note that in your code, you will insert "false" even if the client has set a value to "0", or an empty string).
  • Don't go formatting your own dates to stuff them into the database - use your databases' timestamp feature. And if you need to format times for something else, just use POSIX::strftime. Your entire block of code to format the date could have been written as:
    use POSIX; $date = POSIX::strftime("%A, %B %e, %Y at %T", localtime);
Re: Hacker Proofing My First Script
by thospel (Hermit) on Sep 30, 2004 at 12:35 UTC
    Lots of good advice already, folow it! But I don't yet see the most important security measure for CGI scripts of them all: the -T option. See perldoc perlsec. But don't assume it's a magic bullet. It e.g. won't protect you from SQL injection problems.

    Another major aspect is never visible in the script source code, and that is to set up your server so the CGI never runs as the web server user. For every task set up a different fake user and use something like suexec to run your CGI as that user. That way if you do have a hole, your server isn't directly compromised (though even getting local user access is already a huge step for a hacker). Here it will also allow you to set the permissions of your SQL server so that this user (and therefore CGI script) may only do the things he needs to do here. That DOES help somewhat against SQL injection (though the real solution to that is still using placeholders) and even against someone taking over your script and now trying to access the SQL server.

Re: Hacker Proofing My First Script
by muntfish (Chaplain) on Sep 30, 2004 at 13:05 UTC

    I just have one point to add to the excellent advice given so far. It's actually database related, not anything in your Perl script:

    Make sure that the database userid is as unprivileged as possible. I notice you're using mysql and I don't know much about how that compares to other RDBMS but I assume you can have different levels of user, such as sa/dbo/normal, and that you can grant and/or revoke permissions for select/insert/update on individual tables. So make this user as "low" as you can and make sure it doesn't have permission to do anything except insert into the speedtrap table.

    Of course, if I'm talking cobblers regarding mysql, then apologies...


    s^^unp(;75N=&9I<V@`ack(u,^;s|\(.+\`|"$`$'\"$&\"\)"|ee;/m.+h/&&print$&
Re: Hacker Proofing My First Script
by Fletch (Bishop) on Sep 30, 2004 at 13:36 UTC

    More of a style nit, but all of your if (!$INPUT{'radar'}) {$INPUT{'radar'} = "false"}; statements could be replaced with:

    use constant TECHNOLOGY_TYPES => qw( radar laser vascar airplane photo roadblock unknown ); for my $tech ( TECHNOLOGY_TYPES() ) { $INPUT{ $tech } ||= "false"; }

    Of course that sidesteps the problem that you've used CGI and yet for some reason are doing your own query parsing (which is in itself a red flag) . . .

Re: Hacker Proofing My First Script
by jwest (Friar) on Oct 01, 2004 at 18:50 UTC
    As an aside, the technique that you've used, scrubbing the data of harmful characters, is known as a negative security model. This means that you're allowing everything, except for the few things that you're explicitly denying.

    This is, arguably, a sub-optimal model to follow from a security perspective. Over time as more creative ways of breaking applications are developed you may find that a character you didn't account for can be used against you. Similarly, not all of the harmful characters may be known in advance, given a different application.

    Most in the security field would argue that a positive security model is a more practical approach. In a positive security model, you deny anything that is not explicitly allowed. Instead of saying that ? should be stripped, for example, you only allow the characters that you know to be legitimate through.

    As others have pointed out, there are better approaches still to your problem.

    --jwest


    -><- -><- -><- -><- -><-
    All things are Perfect
        To every last Flaw
        And bound in accord
             With Eris's Law
     - HBT; The Book of Advice, 1:7
    
Re: Hacker Proofing My First Script
by ggg (Scribe) on Sep 30, 2004 at 17:14 UTC
    The sheer volumn of quality replies one gets here never ceases to amaze me! I love this place!

    ggg
Re: Hacker Proofing My First Script
by CountZero (Bishop) on Sep 30, 2004 at 21:56 UTC
    As you are (or should be) only using CGI for parsing the input and generating a redirection header, perhaps you can think of using CGI::Simple instead of CGI. It has the same functionality except for all the HTML-generating stuff, which you do not seem to be using anyhow and which by many is considered to be the weaker point of the CGI-module.

    CountZero

    "If you have four groups working on a compiler, you'll get a 4-pass compiler." - Conway's Law

Re: Hacker Proofing My First Script
by Anonymous Monk on Oct 01, 2004 at 23:36 UTC
    Do NOT quote the quote-symbols ('") automatically with backslashes in your input(!)-routine. One day you may want to create a table with data from the user. I learned it the hard way (phpmyadmin with magic-quotes on). Btw you forgot to quote the \