{"id":18089005,"url":"https://github.com/eriksjolund/writing-more-secure-perl-cgi-scripts","last_synced_at":"2025-04-06T02:28:51.771Z","repository":{"id":92823551,"uuid":"267237588","full_name":"eriksjolund/writing-more-secure-perl-cgi-scripts","owner":"eriksjolund","description":"Tutorial of how to write more secure Perl CGI scripts","archived":false,"fork":false,"pushed_at":"2020-05-27T06:47:28.000Z","size":5,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-12T08:39:05.698Z","etag":null,"topics":["cgi","perl","perl5","security","security-hardening","tutorial"],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/eriksjolund.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-05-27T06:21:05.000Z","updated_at":"2022-10-06T01:48:20.000Z","dependencies_parsed_at":"2023-03-20T09:32:12.044Z","dependency_job_id":null,"html_url":"https://github.com/eriksjolund/writing-more-secure-perl-cgi-scripts","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eriksjolund%2Fwriting-more-secure-perl-cgi-scripts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eriksjolund%2Fwriting-more-secure-perl-cgi-scripts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eriksjolund%2Fwriting-more-secure-perl-cgi-scripts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eriksjolund%2Fwriting-more-secure-perl-cgi-scripts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eriksjolund","download_url":"https://codeload.github.com/eriksjolund/writing-more-secure-perl-cgi-scripts/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247425538,"owners_count":20936971,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cgi","perl","perl5","security","security-hardening","tutorial"],"created_at":"2024-10-31T17:42:35.013Z","updated_at":"2025-04-06T02:28:51.752Z","avatar_url":"https://github.com/eriksjolund.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# writing-more-secure-perl-cgi-scripts\n\nHow to write more secure Perl CGI scripts. A guide with some tips.\n \n__Update March 2020__: Migrate this tutorial from 2008 to Github for archival purposes. (Nowadays there are better programming languages and web frameworks).\n\n## Introduction\n\nThis guide contains some example CGI scripts that demonstrates how to improve security in CGI scripts. The example _count7.cgi_ has the best security.\n\n## Advices\n\nIf you are writing your CGI scripts in perl, you should\n\nuse the `-T` flag. It is also a good idea to use the `-w` flag. The script will hence start with\n\n```\n#!/usr/bin/perl -Tw \n```\n\n\nAvoid using temporary files as much as possible. If you want to call an external program, try to use the `open2()` call. This works under the assumption that the external program can read the input data from standard input and write the result to standard output. For instance, like this\n\n```\nuse IPC::Open2;\nmy $childpid = open2(\\*READ, \\*WRITE, $config::blastall_path , \"-m\", \"7\", \"-d\" , $db, \"-p\", \"blastp\", \"-b\", $max_hits, \"-v\", $max_hits, \"-M\", \"BLOSUM62\"   )\n or die \"Could not open pipe: $!\";\n\nprint WRITE $blastinput;\nclose(WRITE);\n\nmy $blastoutput;\nwhile(\u003cREAD\u003e) {\n  $blastoutput .= $_;\n}\nclose READ;\nwaitpid($childpid, 0);\n```\n\nnever use something like this\n\n```\nsystem(\"myprogram -f $cgiparam\");\n```\n\ninstead use the comma separated syntax\n\n```\nsystem(\"myprogram\",\"-f\",\"$cgiparam\");\n```\n\n## Example CGI scripts\n\n\n### count.py\n\n_count.py_ is a Python script that is used by all of the example cgi scripts.\n\n\n```\n#!/usr/bin/python\n\nimport sys\n\nif len(sys.argv) == 2:\n  sys.exit()\nelif len(sys.argv) == 3:\n  f=open(sys.argv[2], \"r\") \nelse: \n  sys.exit()\n\n#f.seek(0)\n\nlines=0\nbytes=0\nfor line in f:\n  lines += 1\n  bytes += len( line ) \n\nif (sys.argv[1] == \"bytes\"): \n  print str(bytes)\n\nif (sys.argv[1] == \"lines\"): \n  print str(lines)\n```\n\n### count1.cgi\n\nConfiguring __apache httpd__ to allow _Server Side Includes_ ( SSI ) is dangerous. The option is called `Includes`.\n\nA hacker could upload a file with SSI exec statements. Those commands would then be executed as soon as a web browser tries to access the uploaded file. Often SSI is only activated for filenames ending with \"_.shtml_\". In the this CGI script the web surfer chooses the file name and can choose a file name ending with \"_.shtml_\".\n\n```\n#!/usr/bin/perl \n\nuse CGI qw(:standard);\nuse CGI::Carp qw(fatalsToBrowser);\n\nmy $cgi = new CGI;\nif ($cgi-\u003ecgi_error())  { die() };\n\nprint\n  $cgi-\u003eheader(),\n  $cgi-\u003estart_html( '' ),\n  $cgi-\u003eh1('Count'),       \n  $cgi-\u003estart_form(-method=\u003e'GET'),\n  \"type:\",\n  $cgi-\u003epopup_menu(-name=\u003e'type',-values=\u003e['lines','bytes']),\n  $cgi-\u003ep, \n  \"filename\",\n  $cgi-\u003etextfield(-name=\u003e'fname'),\n  $cgi-\u003ep,\n  $cgi-\u003etextarea(-name=\u003e'content', -rows=\u003e10,  -columns=\u003e50),\n  $cgi-\u003ep,\n  $cgi-\u003esubmit(),\n  $cgi-\u003eend_form,\n  $cgi-\u003ehr;\n\nif ($cgi-\u003eparam) {\n\n    my $fname = $cgi-\u003eparam('fname');\n    my $content = $cgi-\u003eparam('content');\n    my $type = $cgi-\u003eparam('type'); \n\n    my $filename=\"tmp/\" . $fname;\n\n    my $outfh;\n    open($outfh, \"\u003e\", $filename) \n        or die \"Couldn't open $tmpfile for writing: $!\";\n\n    print $outfh $content;\n    close $outfh;\n\n    my $result=`/home/esjolund/public_html/cgi-bin/count.py $type $filename`;\n\n    print \"The file \u003cb\u003e$fname\u003c/b\u003e has $result $type\";\n    print $cgi-\u003eend_html;  \n}\n```\n\n### count2.cgi\n\nExamplifies `use File::Temp qw(tempfile);`. If you can't avoid using temporary files, please use this perl module to create them. Although not possible here, you should preferrably use `UNLINK =\u003e 1` as argument to the `tempfile()` function.\n\n```\n#!/usr/bin/perl \n\nuse CGI qw(:standard);\nuse CGI::Carp qw(fatalsToBrowser);\nuse File::Temp qw(tempfile);\n\nmy $cgi = new CGI;\nif ($cgi-\u003ecgi_error())  { die() };\n\nprint\n  $cgi-\u003eheader(),\n  $cgi-\u003estart_html( '' ),\n  $cgi-\u003eh1('Count'),       \n  $cgi-\u003estart_form(-method=\u003e'GET'),\n  \"type:\",\n  $cgi-\u003epopup_menu(-name=\u003e'type',-values=\u003e['lines','bytes']),\n  $cgi-\u003ep,\n  $cgi-\u003etextarea(-name=\u003e'content', -rows=\u003e10,  -columns=\u003e50),\n  $cgi-\u003ep,\n  $cgi-\u003esubmit(),\n  $cgi-\u003eend_form,\n  $cgi-\u003ehr;\n\nif ($cgi-\u003eparam) {\n\n    my $content = $cgi-\u003eparam('content');\n    my $type = $cgi-\u003eparam('type'); \n\n    File::Temp-\u003esafe_level( File::Temp::HIGH );\n    my ( $outfh, $filename ) =\n       tempfile( \"tmp/myfiles.XXXXXX\", UNLINK =\u003e 0 );\n    if ( !defined $outfh ) {\n\tdie \"error: no temporary file\\n\";\n    }\n\n    print $outfh $content;\n    close $outfh;\n\n    my $result=`/home/esjolund/public_html/cgi-bin/count.py $type $filename`;\n\n    print \"Content has $result $type\";\n    print $cgi-\u003eend_html;  \n}\n```\n\n\n### count3.cgi\n\nIt is often possible to avoid temporary files by using pipes. In Perl this is done with `open2()`\n\n```\n#!/usr/bin/perl \n\nuse CGI qw(:standard);\nuse CGI::Carp qw(fatalsToBrowser);\nuse IPC::Open2;\n\nmy $cgi = new CGI;\nif ($cgi-\u003ecgi_error())  { die() };\n\nprint\n  $cgi-\u003eheader(),\n  $cgi-\u003estart_html( '' ),\n  $cgi-\u003eh1('Count'),       \n  $cgi-\u003estart_form(-method=\u003e'GET'),\n  \"type:\",\n  $cgi-\u003epopup_menu(-name=\u003e'type',-values=\u003e['lines','bytes']),\n  $cgi-\u003ep,\n  $cgi-\u003etextarea(-name=\u003e'content', -rows=\u003e10,  -columns=\u003e50),\n  $cgi-\u003ep,\n  $cgi-\u003esubmit(),\n  $cgi-\u003eend_form,\n  $cgi-\u003ehr;\n\nif ($cgi-\u003eparam) {\n\n    my $content = $cgi-\u003eparam('content');\n    my $type = $cgi-\u003eparam('type'); \n\n    my($child_out, $child_in);\n    $pid = open2($child_out, $child_in, \"/home/esjolund/public_html/cgi-bin/count.py\", $type,\"/dev/stdin\");\n    print $child_in $content;\n    close($child_in);\n    my $result=\u003c$child_out\u003e;  \n    waitpid($pid,0);\n\n    print \"Content has $result $type\";\n    print $cgi-\u003eend_html;  \n}\n```\n\n\n### count4.cgi\n\nThe previous example `count3.cgi`  is insecure as it passes the `$type` variable as directly to the command line ( to a shell ). We should instead use the comma separated syntax of `open2()` as shown here:\n\n```\n#!/usr/bin/perl \n\nuse CGI qw(:standard);\nuse CGI::Carp qw(fatalsToBrowser);\nuse IPC::Open2;\n\nmy $cgi = new CGI;\nif ($cgi-\u003ecgi_error())  { die() };\n\nprint\n  $cgi-\u003eheader(),\n  $cgi-\u003estart_html( '' ),\n  $cgi-\u003eh1('Count'),       \n  $cgi-\u003estart_form(-method=\u003e'GET'),\n  \"type:\",\n  $cgi-\u003epopup_menu(-name=\u003e'type',-values=\u003e['lines','bytes']),\n  $cgi-\u003ep,\n  $cgi-\u003etextarea(-name=\u003e'content', -rows=\u003e10,  -columns=\u003e50),\n  $cgi-\u003ep,\n  $cgi-\u003esubmit(),\n  $cgi-\u003eend_form,\n  $cgi-\u003ehr;\n\nif ($cgi-\u003eparam) {\n\n    my $content = $cgi-\u003eparam('content');\n    my $type = $cgi-\u003eparam('type'); \n\n    my($child_out, $child_in);\n    $pid = open2($child_out, $child_in, \"/home/esjolund/public_html/cgi-bin/count.py\",$type);\n    print $child_in $content;\n    close($child_in);\n    my $result=\u003c$child_out\u003e;  \n    waitpid($pid,0);\n\n    print \"Content has $result $type\";\n    print $cgi-\u003eend_html;  \n}\n```\n\n\n### count5.cgi\n\nExamplifies taint mode, the `-T` flag\n\n```\n#!/usr/bin/perl -T\n\nuse CGI qw(:standard);\nuse CGI::Carp qw(fatalsToBrowser);\nuse IPC::Open2;\n\nuse Scalar::Util qw(tainted);\n\nmy $cgi = new CGI;\nif ($cgi-\u003ecgi_error())  { die() };\n\nprint\n  $cgi-\u003eheader(),\n  $cgi-\u003estart_html( '' ),\n  $cgi-\u003eh1('Count'),       \n  $cgi-\u003estart_form(-method=\u003e'GET'),\n  \"type:\",\n  $cgi-\u003epopup_menu(-name=\u003e'type',-values=\u003e['lines','bytes']),\n  $cgi-\u003ep,\n  $cgi-\u003etextarea(-name=\u003e'content', -rows=\u003e10,  -columns=\u003e50),\n  $cgi-\u003ep,\n  $cgi-\u003esubmit(),\n  $cgi-\u003eend_form,\n  $cgi-\u003ehr;\n\nif ($cgi-\u003eparam) {\n\n    if (tainted($cgi-\u003eparam('content'))) \n       { print 'variable content tainted!'; } \n    else \n       { print 'variable content not tainted!'; } \n    print `echo $cgi-\u003eparam('content')`;\n\n    my $content = $cgi-\u003eparam('content');\n    my $type = $cgi-\u003eparam('type'); \n\n    my($child_out, $child_in);\n    $pid = open2($child_out, $child_in, \"/home/esjolund/public_html/cgi-bin/count.py\",$type);\n    print $child_in $content;\n    close($child_in);\n    my $result=\u003c$child_out\u003e;  \n    waitpid($pid,0);\n\n    print \"Content has $result $type\";\n    print $cgi-\u003eend_html;  \n}\n```\n\n### count6.cgi\n\nIf we try to run the previous example _count5.cgi_,  perl will complain that the `PATH` variable is insecure. If we use the `-T` flag, we need to set the `PATH` environment variable.\n\n```\n#!/usr/bin/perl -T\n\nuse CGI qw(:standard);\nuse CGI::Carp qw(fatalsToBrowser);\nuse IPC::Open2;\n\nuse Scalar::Util qw(tainted);\n\nmy $cgi = new CGI;\nif ($cgi-\u003ecgi_error())  { die() };\n\n$ENV{'PATH'} = '/bin:/usr/bin';\ndelete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};\n\nprint\n  $cgi-\u003eheader(),\n  $cgi-\u003estart_html( '' ),\n  $cgi-\u003eh1('Count'),       \n  $cgi-\u003estart_form(-method=\u003e'GET'),\n  \"type:\",\n  $cgi-\u003epopup_menu(-name=\u003e'type',-values=\u003e['lines','bytes']),\n  $cgi-\u003ep,\n  $cgi-\u003etextarea(-name=\u003e'content', -rows=\u003e10,  -columns=\u003e50),\n  $cgi-\u003ep,\n  $cgi-\u003esubmit(),\n  $cgi-\u003eend_form,\n  $cgi-\u003ehr;\n\nif ($cgi-\u003eparam) {\n\n    my $content = $cgi-\u003eparam('content');\n    my $type = $cgi-\u003eparam('type'); \n\n    if ( tainted($type) ) \n       { print 'tainted!'; } \n     else \n       { print 'not tainted!'; } \n\n    print $cgi-\u003ehr;\n    print `echo $type`;\n\n    my($child_out, $child_in);\n    $pid = open2($child_out, $child_in, \"/home/esjolund/public_html/cgi-bin/count.py\",$type);\n    print $child_in $content;\n    close($child_in);\n    my $result=\u003c$child_out\u003e;  \n    waitpid($pid,0);\n\n    print \"Content has $result $type\";\n    print $cgi-\u003eend_html;  \n}\n```\n\n### count7.cgi\n\nIf we try to run the previous example _count6.cgi_  perl will complain that we are using untainted variables in an insecure way. To use CGI input variables we need to untaint them by processing them with a regular expression\n\n```\n#!/usr/bin/perl -T\n\nuse CGI qw(:standard);\nuse CGI::Carp qw(fatalsToBrowser);\nuse IPC::Open2;\n\nuse Scalar::Util qw(tainted);\n\nmy $cgi = new CGI;\nif ($cgi-\u003ecgi_error())  { die() };\n\n$ENV{'PATH'} = '/bin:/usr/bin';\ndelete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};\n\nprint\n  $cgi-\u003eheader(),\n  $cgi-\u003estart_html( '' ),\n  $cgi-\u003eh1('Count'),       \n  $cgi-\u003estart_form(-method=\u003e'GET'),\n  \"type:\",\n  $cgi-\u003epopup_menu(-name=\u003e'type',-values=\u003e['lines','bytes']),\n  $cgi-\u003ep,\n  $cgi-\u003etextarea(-name=\u003e'content', -rows=\u003e10,  -columns=\u003e50),\n  $cgi-\u003ep,\n  $cgi-\u003esubmit(),\n  $cgi-\u003eend_form,\n  $cgi-\u003ehr;\n\nif ($cgi-\u003eparam) {\n\n    my $type;\n    if ( $cgi-\u003eparam('type')=~/^(bytes|lines)$/ ) {\n        $type = $1; \n    } else {\n        print \"variable type needs to be bytes or lines\";\n        exit(0);\n    }\n\n    my $content;\n    if ( $cgi-\u003eparam('content')=~/^(.*)$/s ) {\n        $content = $1; \n    } else {\n        exit(0);\n    }\n\n    if ( tainted($type) ) \n       { print 'tainted!'; } \n     else \n       { print 'not tainted!'; } \n\n    print $cgi-\u003ehr;\n    print `echo $type`;\n\n    my($child_out, $child_in);\n    $pid = open2($child_out, $child_in, \"/home/esjolund/public_html/cgi-bin/count.py\",$type);\n    print $child_in $content;\n    close($child_in);\n    my $result=\u003c$child_out\u003e;  \n    waitpid($pid,0);\n\n    print \"Content has $result $type\";\n    print $cgi-\u003eend_html;  \n}\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feriksjolund%2Fwriting-more-secure-perl-cgi-scripts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feriksjolund%2Fwriting-more-secure-perl-cgi-scripts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feriksjolund%2Fwriting-more-secure-perl-cgi-scripts/lists"}