PAYATHON 2026

Perl CGI 的 param() 为什么会在列表上下文中注入哈希键?

数据小陈

结论

额外的键会出现在 %user_info 中。重复提交 name 参数,让多个值依次作为姓名、额外键和额外值,就能触发这个问题。

例如:

name=Alice&name=is_admin&name=1

对于题目中的代码,%user_info 会包含 is_admin => '1',尽管请求中没有名为 is_admin 的参数。

这里是向哈希中注入键值对,输入不会作为 Perl 源代码执行。是否进一步造成权限问题,取决于后续代码如何使用这个哈希。

为什么参数值会变成哈希键?

题目引用的警告针对这个调用位置:

my %user_info = (
    id   => 1,
    name => $q->param('name'),
);

param('name') 在列表上下文中返回该参数的所有值,在标量上下文中则返回第一个值。CGI 文档明确说明了这一行为。

=> 不会将右边的表达式限制为单个值。哈希赋值的右侧仍然是一个列表,函数返回的多个元素会在其中展开,Perl 再将相邻的两个元素配成键值对。相关规则见 Perl 的列表与哈希赋值说明。

对于上面的 query string,调用返回的列表是:

('Alice', 'is_admin', '1')

整个赋值相当于:

my %user_info = (
    'id',   1,
    'name', 'Alice',
    'is_admin', '1',
);

按位置配对后,is_admin 成为哈希键。它在 HTTP 请求中是 name 的第二个值,在构造哈希时占据了键的位置。

仅提交 name=Alice&is_admin=1,不会让这段只读取 param('name') 的代码出现相同效果。

如何复现?

在已安装 CGI 模块的 Perl 环境中,可以直接用 query string 初始化对象,无需先搭建 Web 服务:

use strict;
use warnings;
use CGI ();

my $q = CGI->new('name=Alice&name=is_admin&name=1');

my %user_info = (
    id   => 1,
    name => $q->param('name'),
);

for my $key (sort keys %user_info) {
    print "$key=$user_info{$key}\n";
}

根据上述配对规则,打印结果应为:

id=1
is_admin=1
name=Alice

CGI.pm 还可能输出列表上下文安全警告。该警告用于提示风险,不会自动阻止列表返回。

如果通过 HTTP 复现,GET 请求的查询部分可以写成:

?name=Alice&name=is_admin&name=1

使用 Content-Type: application/x-www-form-urlencoded 的 POST 请求时,请求体可以使用同一串数据:

name=Alice&name=is_admin&name=1

能否覆盖已有的键?

可以,前提是注入的同名键值对出现在原有键值对之后。哈希赋值时,后出现的同名键会覆盖前面的值。

例如,将请求改为:

name=Alice&name=id&name=999

原来的赋值就相当于:

my %user_info = (
    id   => 1,
    name => 'Alice',
    id   => '999',
);

最终 id 对应的是 '999'。即使某个字段最初由程序固定赋值,也可能被后面展开的输入覆盖。

如何修复?

如果 name 只需要一个值,可以显式使用 scalar:

my %user_info = (
    id   => 1,
    name => scalar $q->param('name'),
);

也可以先赋给标量变量,再构造哈希:

my $name = $q->param('name');

my %user_info = (
    id   => 1,
    name => $name,
);

这两种写法都让 param('name') 处于标量上下文。对于示例请求,只有 'Alice' 会进入 name 字段。

如果业务允许多个姓名,应显式读取多个值,并将数组引用放入哈希:

my @names = $q->multi_param('name');

my %user_info = (
    id    => 1,
    names => \@names,
);

数组引用在外层列表中只占一个元素,因此不会把姓名列表展开为额外的键值对。

直接把原代码中的 param() 换成 multi_param() 仍会返回列表,无法修复这里的哈希赋值问题。关闭警告也不会改变这一行为。scalar 控制返回值数量,字段格式校验、是否拒绝重复参数和权限判断仍需按业务要求处理。

备注:内容仅供参考。