Perl如何处理CGI文件上传 CGI.pm模块的使用

因为 CGI.pm 默认将文件上传字段当作普通参数处理,$cgi->param('file') 只返回文件名(甚至为空),必须用 $cgi->upload('file_field_name') 获取文件句柄;表单需设 enctype="multipart/form-data",否则 upload() 返回 undef。

CGI.pm 读取上传文件时为何 $cgi->param('file') 返回空?

因为 CGI.pm 默认把文件上传字段当作普通参数处理,实际文件内容需要通过 $cgi->upload() 获取。直接用 param() 只能得到文件名(在部分浏览器下甚至为空),不是文件句柄。

  • 必须用 $cgi->upload('file_field_name') 获取可读文件句柄,而非 $cgi->param()
  • 表单 name 属性值必须和 upload() 的参数一致
  • HTML 表单 必须 设置 enctype="multipart/form-data",否则 upload() 返回 undef

如何安全保存上传的文件到磁盘?

不能直接用用户提交的原始文件名,需过滤路径遍历(如 ../etc/passwd)和非法字符。Perl 自身不自动清理文件名,得手动处理。

  • File::Basename::basename() 提取纯文件名,丢弃路径部分
  • 用正则 s/[^a-zA-Z0-9._-]+//g 删除危险字符(保留字母、数字、点、下划线、短横)
  • 检查扩展名是否在白名单中(如 qw(jpg png pdf txt)),避免执行型文件上传
  • open my $fh, '>', $safe_path 写入,不要>> 追加,防止覆盖关键文件
use File::Basename;
my $upload = $cgi->upload('myfile');
my $filename = basename($cgi->param('myfile'));
$filename =~ s/[^a-zA-Z0-9._-]+//g;
my $ext = lc((split /\./, $filename)[-1] // '');
die "Invalid extension" unless grep { $_ eq $ext } qw(txt jpg png pdf);
open my $out, '>', "/var/www/uploads/$filename" or die "Cannot open: $!";
binmode $out;
while (my $bytesread = read($upload, my $buffer, 8192)) {
    print $out $buffer;
}
close $out;

C

GI.pm 的 upload() 返回什么类型?

返回一个 Perl 文件句柄(GLOB ref),行为类似 open FH, ' 得到的句柄,支持 read()binmode(),但不支持 行读取(因是二进制流)。

  • 必须调用 binmode($fh),否则 Windows 换行或非 ASCII 字符会损坏
  • read($fh, $buf, $len) 是推荐读法;sysread() 也可用,但需自行处理 EOF 和错误
  • 若上传字段不存在或出错,upload() 返回 undef,需提前检查
  • 该句柄只可读一次;重复调用 upload() 不会重置位置,也不会重新读取

为什么 CGI.pm 已被弃用?现代替代方案是什么?

CGI.pm 自 Perl 5.20 起标记为 deprecated,5.32+ 默认不安装,且不支持 PSGI/Plack,无法用于 FastCGI、mod_perl 或现代 Web 服务器部署。

  • 推荐迁移到 Plack::Request(配合 Plack 中间件):上传文件自动解析为临时文件或 IO::Handle 对象
  • 若必须维持 CGI 环境,可用 CGI::Simple(轻量兼容)或手写 parse_multipart()(用 HTTP::Body
  • 注意:所有替代方案仍要求表单 enctype="multipart/form-data" 和正确 Content-Length

最易忽略的一点:CGI.pm 在调试时不会报错提示 enctype 缺失,只会让 upload() 静默返回 undef —— 建议始终先 defined $fh or die "No file uploaded"