วิธีเขียนสคริปต์ Perl CGI ที่ปลอดภัยยิ่งขึ้น คำแนะนำพร้อมเคล็ดลับ
อัปเดตมีนาคม 2563 : โยกย้ายบทช่วยสอนนี้จากปี 2008 ไปยัง GitHub เพื่อวัตถุประสงค์ในการเก็บถาวร (ทุกวันนี้มีภาษาการเขียนโปรแกรมที่ดีขึ้นและเฟรมเวิร์กเว็บ)
คู่มือนี้มีตัวอย่างสคริปต์ CGI ที่แสดงวิธีปรับปรุงความปลอดภัยในสคริปต์ CGI ตัวอย่าง count7.cgi มีความปลอดภัยที่ดีที่สุด
หากคุณกำลังเขียนสคริปต์ CGI ของคุณใน Perl คุณควร
ใช้ธง -T นอกจากนี้ยังเป็นความคิดที่ดีที่จะใช้ธง -w สคริปต์จะเริ่มต้นด้วย
#!/usr/bin/perl -Tw
หลีกเลี่ยงการใช้ไฟล์ชั่วคราวให้มากที่สุด หากคุณต้องการโทรหาโปรแกรมภายนอกให้ลองใช้การโทร open2() สิ่งนี้ทำงานภายใต้สมมติฐานที่ว่าโปรแกรมภายนอกสามารถอ่านข้อมูลอินพุตจากอินพุตมาตรฐานและเขียนผลลัพธ์ไปยังเอาต์พุตมาตรฐาน ตัวอย่างเช่นนี้
use IPC::Open2;
my $childpid = open2(*READ, *WRITE, $config::blastall_path , "-m", "7", "-d" , $db, "-p", "blastp", "-b", $max_hits, "-v", $max_hits, "-M", "BLOSUM62" )
or die "Could not open pipe: $!";
print WRITE $blastinput;
close(WRITE);
my $blastoutput;
while(<READ>) {
$blastoutput .= $_;
}
close READ;
waitpid($childpid, 0);
อย่าใช้อะไรแบบนี้
system("myprogram -f $cgiparam");
ใช้ไวยากรณ์คั่นด้วยเครื่องหมายจุลภาคแทน
system("myprogram","-f","$cgiparam");
count.py เป็นสคริปต์ Python ที่ใช้โดยสคริปต์ตัวอย่าง CGI ทั้งหมด
#!/usr/bin/python
import sys
if len(sys.argv) == 2:
sys.exit()
elif len(sys.argv) == 3:
f=open(sys.argv[2], "r")
else:
sys.exit()
#f.seek(0)
lines=0
bytes=0
for line in f:
lines += 1
bytes += len( line )
if (sys.argv[1] == "bytes"):
print str(bytes)
if (sys.argv[1] == "lines"):
print str(lines)
การกำหนดค่า Apache httpd เพื่อให้ ฝั่งเซิร์ฟเวอร์รวมถึง (SSI) เป็นสิ่งที่อันตราย ตัวเลือกที่เรียกว่า Includes
แฮ็กเกอร์สามารถอัปโหลดไฟล์ด้วยคำสั่ง SSI EXEC คำสั่งเหล่านั้นจะถูกดำเนินการทันทีที่เว็บเบราว์เซอร์พยายามเข้าถึงไฟล์ที่อัปโหลด บ่อยครั้งที่ SSI เปิดใช้งานเฉพาะชื่อไฟล์ที่ลงท้ายด้วย " .shtml " ในสคริปต์ CGI นี้ Web Surfer จะเลือกชื่อไฟล์และสามารถเลือกชื่อไฟล์ที่ลงท้ายด้วย " .shtml "
#!/usr/bin/perl
use CGI qw(:standard);
use CGI::Carp qw(fatalsToBrowser);
my $cgi = new CGI;
if ($cgi->cgi_error()) { die() };
print
$cgi->header(),
$cgi->start_html( '' ),
$cgi->h1('Count'),
$cgi->start_form(-method=>'GET'),
"type:",
$cgi->popup_menu(-name=>'type',-values=>['lines','bytes']),
$cgi->p,
"filename",
$cgi->textfield(-name=>'fname'),
$cgi->p,
$cgi->textarea(-name=>'content', -rows=>10, -columns=>50),
$cgi->p,
$cgi->submit(),
$cgi->end_form,
$cgi->hr;
if ($cgi->param) {
my $fname = $cgi->param('fname');
my $content = $cgi->param('content');
my $type = $cgi->param('type');
my $filename="tmp/" . $fname;
my $outfh;
open($outfh, ">", $filename)
or die "Couldn't open $tmpfile for writing: $!";
print $outfh $content;
close $outfh;
my $result=`/home/esjolund/public_html/cgi-bin/count.py $type $filename`;
print "The file <b>$fname</b> has $result $type";
print $cgi->end_html;
}
การตรวจสอบ use File::Temp qw(tempfile); - หากคุณไม่สามารถหลีกเลี่ยงการใช้ไฟล์ชั่วคราวได้โปรดใช้โมดูล Perl นี้เพื่อสร้าง แม้ว่าจะเป็นไปไม่ได้ที่นี่คุณควรใช้ UNLINK => 1 เป็นอาร์กิวเมนต์ไปยังฟังก์ชัน tempfile()
#!/usr/bin/perl
use CGI qw(:standard);
use CGI::Carp qw(fatalsToBrowser);
use File::Temp qw(tempfile);
my $cgi = new CGI;
if ($cgi->cgi_error()) { die() };
print
$cgi->header(),
$cgi->start_html( '' ),
$cgi->h1('Count'),
$cgi->start_form(-method=>'GET'),
"type:",
$cgi->popup_menu(-name=>'type',-values=>['lines','bytes']),
$cgi->p,
$cgi->textarea(-name=>'content', -rows=>10, -columns=>50),
$cgi->p,
$cgi->submit(),
$cgi->end_form,
$cgi->hr;
if ($cgi->param) {
my $content = $cgi->param('content');
my $type = $cgi->param('type');
File::Temp->safe_level( File::Temp::HIGH );
my ( $outfh, $filename ) =
tempfile( "tmp/myfiles.XXXXXX", UNLINK => 0 );
if ( !defined $outfh ) {
die "error: no temporary filen";
}
print $outfh $content;
close $outfh;
my $result=`/home/esjolund/public_html/cgi-bin/count.py $type $filename`;
print "Content has $result $type";
print $cgi->end_html;
}
มักจะเป็นไปได้ที่จะหลีกเลี่ยงไฟล์ชั่วคราวโดยใช้ท่อ ใน perl สิ่งนี้ทำด้วย open2()
#!/usr/bin/perl
use CGI qw(:standard);
use CGI::Carp qw(fatalsToBrowser);
use IPC::Open2;
my $cgi = new CGI;
if ($cgi->cgi_error()) { die() };
print
$cgi->header(),
$cgi->start_html( '' ),
$cgi->h1('Count'),
$cgi->start_form(-method=>'GET'),
"type:",
$cgi->popup_menu(-name=>'type',-values=>['lines','bytes']),
$cgi->p,
$cgi->textarea(-name=>'content', -rows=>10, -columns=>50),
$cgi->p,
$cgi->submit(),
$cgi->end_form,
$cgi->hr;
if ($cgi->param) {
my $content = $cgi->param('content');
my $type = $cgi->param('type');
my($child_out, $child_in);
$pid = open2($child_out, $child_in, "/home/esjolund/public_html/cgi-bin/count.py", $type,"/dev/stdin");
print $child_in $content;
close($child_in);
my $result=<$child_out>;
waitpid($pid,0);
print "Content has $result $type";
print $cgi->end_html;
}
ตัวอย่างก่อนหน้า count3.cgi ไม่ปลอดภัยเนื่องจากผ่านตัวแปร $type เป็นโดยตรงไปยังบรรทัดคำสั่ง (ไปยังเชลล์) เราควรใช้ไวยากรณ์คั่นด้วยเครื่องหมายจุลภาคของ open2() แทนที่แสดงที่นี่:
#!/usr/bin/perl
use CGI qw(:standard);
use CGI::Carp qw(fatalsToBrowser);
use IPC::Open2;
my $cgi = new CGI;
if ($cgi->cgi_error()) { die() };
print
$cgi->header(),
$cgi->start_html( '' ),
$cgi->h1('Count'),
$cgi->start_form(-method=>'GET'),
"type:",
$cgi->popup_menu(-name=>'type',-values=>['lines','bytes']),
$cgi->p,
$cgi->textarea(-name=>'content', -rows=>10, -columns=>50),
$cgi->p,
$cgi->submit(),
$cgi->end_form,
$cgi->hr;
if ($cgi->param) {
my $content = $cgi->param('content');
my $type = $cgi->param('type');
my($child_out, $child_in);
$pid = open2($child_out, $child_in, "/home/esjolund/public_html/cgi-bin/count.py",$type);
print $child_in $content;
close($child_in);
my $result=<$child_out>;
waitpid($pid,0);
print "Content has $result $type";
print $cgi->end_html;
}
ตรวจสอบโหมด Taint, -T Flag
#!/usr/bin/perl -T
use CGI qw(:standard);
use CGI::Carp qw(fatalsToBrowser);
use IPC::Open2;
use Scalar::Util qw(tainted);
my $cgi = new CGI;
if ($cgi->cgi_error()) { die() };
print
$cgi->header(),
$cgi->start_html( '' ),
$cgi->h1('Count'),
$cgi->start_form(-method=>'GET'),
"type:",
$cgi->popup_menu(-name=>'type',-values=>['lines','bytes']),
$cgi->p,
$cgi->textarea(-name=>'content', -rows=>10, -columns=>50),
$cgi->p,
$cgi->submit(),
$cgi->end_form,
$cgi->hr;
if ($cgi->param) {
if (tainted($cgi->param('content')))
{ print 'variable content tainted!'; }
else
{ print 'variable content not tainted!'; }
print `echo $cgi->param('content')`;
my $content = $cgi->param('content');
my $type = $cgi->param('type');
my($child_out, $child_in);
$pid = open2($child_out, $child_in, "/home/esjolund/public_html/cgi-bin/count.py",$type);
print $child_in $content;
close($child_in);
my $result=<$child_out>;
waitpid($pid,0);
print "Content has $result $type";
print $cgi->end_html;
}
หากเราพยายามเรียกใช้ตัวอย่างก่อนหน้า Count5.CGI Perl จะบ่นว่าตัวแปรพา PATH นั้นไม่ปลอดภัย หากเราใช้การตั้งค่าสถานะ -T เราต้องตั้งค่าตัวแปรสภาพแวดล้อม PATH
#!/usr/bin/perl -T
use CGI qw(:standard);
use CGI::Carp qw(fatalsToBrowser);
use IPC::Open2;
use Scalar::Util qw(tainted);
my $cgi = new CGI;
if ($cgi->cgi_error()) { die() };
$ENV{'PATH'} = '/bin:/usr/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
print
$cgi->header(),
$cgi->start_html( '' ),
$cgi->h1('Count'),
$cgi->start_form(-method=>'GET'),
"type:",
$cgi->popup_menu(-name=>'type',-values=>['lines','bytes']),
$cgi->p,
$cgi->textarea(-name=>'content', -rows=>10, -columns=>50),
$cgi->p,
$cgi->submit(),
$cgi->end_form,
$cgi->hr;
if ($cgi->param) {
my $content = $cgi->param('content');
my $type = $cgi->param('type');
if ( tainted($type) )
{ print 'tainted!'; }
else
{ print 'not tainted!'; }
print $cgi->hr;
print `echo $type`;
my($child_out, $child_in);
$pid = open2($child_out, $child_in, "/home/esjolund/public_html/cgi-bin/count.py",$type);
print $child_in $content;
close($child_in);
my $result=<$child_out>;
waitpid($pid,0);
print "Content has $result $type";
print $cgi->end_html;
}
หากเราพยายามเรียกใช้ตัวอย่างก่อนหน้า Count6.cgi Perl จะบ่นว่าเราใช้ตัวแปรที่ไม่ได้ทาสีในลักษณะที่ไม่ปลอดภัย ในการใช้ตัวแปรอินพุต CGI เราจำเป็นต้องไม่ได้ตั้งใจโดยการประมวลผลด้วยนิพจน์ทั่วไป
#!/usr/bin/perl -T
use CGI qw(:standard);
use CGI::Carp qw(fatalsToBrowser);
use IPC::Open2;
use Scalar::Util qw(tainted);
my $cgi = new CGI;
if ($cgi->cgi_error()) { die() };
$ENV{'PATH'} = '/bin:/usr/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
print
$cgi->header(),
$cgi->start_html( '' ),
$cgi->h1('Count'),
$cgi->start_form(-method=>'GET'),
"type:",
$cgi->popup_menu(-name=>'type',-values=>['lines','bytes']),
$cgi->p,
$cgi->textarea(-name=>'content', -rows=>10, -columns=>50),
$cgi->p,
$cgi->submit(),
$cgi->end_form,
$cgi->hr;
if ($cgi->param) {
my $type;
if ( $cgi->param('type')=~/^(bytes|lines)$/ ) {
$type = $1;
} else {
print "variable type needs to be bytes or lines";
exit(0);
}
my $content;
if ( $cgi->param('content')=~/^(.*)$/s ) {
$content = $1;
} else {
exit(0);
}
if ( tainted($type) )
{ print 'tainted!'; }
else
{ print 'not tainted!'; }
print $cgi->hr;
print `echo $type`;
my($child_out, $child_in);
$pid = open2($child_out, $child_in, "/home/esjolund/public_html/cgi-bin/count.py",$type);
print $child_in $content;
close($child_in);
my $result=<$child_out>;
waitpid($pid,0);
print "Content has $result $type";
print $cgi->end_html;
}