在另一篇文章中,我展示了以下Perl CGI脚本。评论中的某人说


“您使用了很多非常糟糕的语法,我想给您一些反馈”。


那么,就在这里,告诉我如何使它更Perlish?我来自C语言背景,所以全是耳朵(或眼睛)!

#!/usr/bin/perl -T

use strict;
use warnings;
use CGI qw/:all/;

my $basepath = '/some/path/to';
my $f = param("f") || '';

($f eq '') && die;

my $path = "$basepath/$f.pdf";

open(PATH, "<$path") || die;
binmode PATH;

print header(-type => "application/pdf",
         -target => "$f");
print;

while (<PATH>) {
    print($_);
}
close(PATH);


#1 楼

我将逐行检查您的代码并提供反馈。我们将跳过关于不使用CGI的一般建议,因为它实际上适合您在此处尝试做的事情。被其他人说。我试图提供完整的反馈,但我没有从他人那里抄袭。如果我们中的几个人提供相同的反馈,则可能必须有一定道理。

#!/usr/bin/perl -T



-T标志打开污染模式。我不确定您打算这样做,还是只是从其他地方复制了它。污染模式实质上可确保必须先验证从外部进入程序的数据,然后再使用它们。但是您并没有保留变量$f。可能会咬你的背。



use strict;
use warnings;



这很好!做得好。您的代码应始终以strictwarnings开头。



use CGI qw/:all/;



您要将很多东西导入名称空间,但您只使用param。相反,可以考虑仅导入:cgi,这将减少您的工作量,或者仅导入paramheader。这也使人们更容易记住该函数的来源。

use CGI qw(param header);




my $f = param("f") || '';



$f不是最大的变量名称。使用描述性变量名,例如$filename。如果文件名为零00000,则此操作将失败。在本文结尾处了解有关此内容的更多信息。


($f eq '') && die;



有些人喜欢使用逻辑运算符进行流程控制,这真的很奇怪。如果您希望简洁的语法没有块,请使用后缀ifunless

此外,请提供一条错误消息,说明失败的原因。这是CGI,因此它将显示在您的服务器日志中。您不希望在第10行说死,是吗?

我更喜欢对空字符串使用q{}引号运算符,因为它显示了更清晰地给出空字符串的意图。

die "No filename given" if $f eq q{}.





open(PATH, "<$path") || die;
不要将GLOB用作文件句柄。它对所有程序都是全局的,包括其他模块和名称空间。如果另一个模块要对句柄使用相同的PATH名称,则会发生冲突。而是使用词汇文件句柄。约定是命名文件句柄$fh。这具有附加的好处,即一旦变量超出范围,Perl就会为您关闭句柄。在这种情况下,程序结束了。因此,您不必显式使用close

使用open的三参数形式,其中第二个参数是模式。在您的情况下,阅读时为<。这使文件操作更安全,因为您的$path可以例如以管道|开头,这将改变含义。特别是如果您的文件名的一部分来自用户(甚至没有取消过它),这是至关重要的建议。 。请改用||,因为绑定非常松散,因此您不需要在or后面加上括号。它也会读得更像实际的英文。

open中包含一条有意义的错误消息,尤其是从die调用返回的错误$!

open my $fh, '<', $path or die "Error opening '$path': $!";
binmode $fh;




print header(-type => "application/pdf",
         -target => "$f");



正确缩进代码。这看起来很奇怪。您已缩进第二行9个空格。看起来像vim中的一个完整选项卡和一个空格。取而代之的是,将其正确地折断并对齐。平均而言,读取的代码是编写的代码的十倍。为此做准备。

如果不需要插值,请不要引用变量。如果只有一个变量,请不要使用引号。

print header(
    -type   => "application/pdf",
    -target => $filename,
);




print;



该行将打印open的当前值。我不确定程序执行时这是什么。这可能不是故意的。摆脱它。



while (<PATH>) {
    print($_);
}



如果要使用循环变量,请对其进行命名。 $_主题对于短单线,$_等确实非常有用,但是map循环很少出现这种情况。

while ( my $buffer = <$fh> ) {
    print $buffer;
}


您也可以使用上述文章-fix表示法。同样,您不需要使用该主题,因为它是隐式的。

print while <$fh>;


但是您也可以完全摆脱该主题,而只需使用以下事实: while是列表上下文,菱形运算符print以列表上下文形式将完整文件返回为数组。

print <$fh>;



我之前说过,您对<>的检查如果名称为假值,则使用$filename的操作将失败。请记住,Perl没有实际的||true,只是评估真实性的值,而不是评估真实性的值。在Perl中很少有错误的事情。 false0,空字符串undef和空列表q{}就是这种情况。 />
my $filename = param('f');
die unless $filename;


如果没有参数(),则0函数返回f。所以我们可以通过说param来避免这种情况。这让undef通过。但是空字符串呢?

本质上,您需要确定名为unless defined的文件是否有效。随你(由你决定。如果您不希望这样做,则必须进行其他检查。

还可以使用0.pdf检查该文件是否存在。

my $filename = param('f');
die unless defined $filename;


但是,无论如何$basepath/.pdf都会进行检查,因此不需要。

评论


\ $ \ begingroup \ $
打印,而<$ fh>一次仅缓冲一行。 print <$ fh>是否一次行缓冲,还是将整个文档读入内存?
\ $ \ endgroup \ $
–马克
17年11月21日在21:19

\ $ \ begingroup \ $
是后者。
\ $ \ endgroup \ $
–乔罗巴
17年11月22日在8:27

\ $ \ begingroup \ $
我对使用菱形运算符读取二进制文件至关重要(是的,尽管PDF主要由明文命令组成,但流和最终流之间的所有内容(例如,都是二进制文件,因此binmode可以)。如perlopentut(1)所述,您可能应该改用read()。
\ $ \ endgroup \ $
–杜布
17年11月22日在10:02



\ $ \ begingroup \ $
-target => $ filename;末尾不得使用分号。
\ $ \ endgroup \ $
–天花板
19年4月16日在9:58

\ $ \ begingroup \ $
@ceving哦,对。我已经解决了。感谢您指出。
\ $ \ endgroup \ $
–simbabque
19年4月16日在10:59

#2 楼

#!/usr/bin/perl -T
use strict;
use warnings;

use CGI qw/ :all /;

my $basepath = '/some/path/to';
my $f = param('f') || '';                  # No interpolation, no double quotes.

die if $f eq '';                           # Don't use && for flow control.

my $path = "$basepath/$f.pdf";

open my $PATH, '<', $path or die;          # Lexical filehandles, 3-argument open.
binmode $PATH;

print header(-type   => 'application/pdf', # Single quotes.
             -target => $f);               # No quotes needed at all.
# print;                                   # This prints $_, are you sure?

print while <$PATH>;
close $PATH;


我还将为die提供一个参数,以便您可以更轻松地搜索日志。

#3 楼

您的param('f')是用户提供的,因此可能引发安全问题。特别是,它可以用于导航到$basepath/../../some-secret-file.pdf。减少了影响,因为您只能读取以.pdf结尾的文件,但它基本上仍然存在。

作为缓解措施,您应限制允许的文件名并与之匹配。即明确验证您的文件名是否正确,不要仅仅尝试排除一些已知的错误模式。例如,在这种情况下,您可能希望允许任何仅由ASCII字母或数字组成的文件名:

unless ($filename =~ /\A[a-zA-Z0-9]+\z/) {
  die "Illegal file name was provided: $filename";
}


(断言\A\z开头是/字符串的结尾,因此请确保整个字符串与该模式匹配,并且不仅在某处包含此模式。)

而不是die,对于用户而言,它会作为500内部服务器错误显示,显式返回一些4xx错误可能更合适。

评论


\ $ \ begingroup \ $
-T本质上应该在这里。
\ $ \ endgroup \ $
–simbabque
17年11月22日在13:58

\ $ \ begingroup \ $
@simbabque Perl认为从具有污名的文件中读取是可以的。因此,异味模式将不会阻止所提出的安全问题。这是一项强大的安全功能,但不是灵丹妙药。
\ $ \ endgroup \ $
–阿蒙
17年11月22日在14:09

\ $ \ begingroup \ $
@amon是的,我知道安全问题。这就是为什么我的CGI脚本没有在用户指定整个文件名的情况下而是在文件名后附加“ .pdf”扩展名,因此,更糟糕的是,恶意用户可以打开PDF文件,而不仅仅是打开任何文件。在另一篇文章中,这是实际脚本的简化版本。为了简洁起见,我省略了错误检查和安全检查处理代码,以便更清楚地显示我所提出的实际问题。
\ $ \ endgroup \ $
– amblabs
17年11月24日在8:40

#4 楼

使用原始标头将“内联”和文件名结合在一起:

print $q->header(
  -type   => "application/pdf",
  -target => $file,
  '-Content-Disposition' => "inline; filename=\"$file\"",
);