TL;博士
停止。立即搁置您的脚本。这是一个等待被利用的巨大安全漏洞。阅读以下资源:
当您阅读并理解了所有这些内容时,请停下来想想您是否真的需要让用户将文件上传到您的服务器上。认真思考。_ 你真的能解释所有列出的漏洞吗?如果您仍然觉得需要这样做,请考虑寻求安全专家的帮助。请仔细遵循上述资源中列出的指南,并了解您的设计中的错误可能会危及您的整个网站。
我知道这只是一个测试脚本,而不是一个生产应用程序(至少,我真的希望是这样),但即便如此,你正在做的事情(尤其是你是如何做的)是一个非常非常 糟糕的主意. 以下是OWASP 的Unrestricted File Upload页面中选择的几个原因:
- 该网站可能会被污损。
- 可以通过上传和执行 web-shell 来入侵 web 服务器,该 web-shell 可以:运行命令、浏览系统文件、浏览本地资源、攻击其他服务器以及利用本地漏洞等。
- 此漏洞可能使网站容易受到 XSS 等其他类型的攻击。
- 可以通过将恶意文件上传到服务器来利用本地文件包含漏洞。
来自 OWASP 的更多信息:
上传的文件对应用程序构成重大风险。许多攻击的第一步是获取要攻击的系统的一些代码。然后攻击只需要找到一种方法来执行代码。使用文件上传有助于攻击者完成第一步。
不受限制的文件上传的后果可能会有所不同,包括完整的系统接管、文件系统过载、将攻击转发到后端系统以及简单的破坏。
很吓人的东西,对吧?
问题
你的代码
让我们从查看您发布的代码的一些问题开始。
没有严格,没有警告
开始将您编写use strict; use warnings;
的每个 Perl 脚本放在首位。我最近有幸修复了一个包含如下片段的 CGI 脚本:
my ($match) = grep { /$usrname/ } @users;
此代码用于检查在 HTML 表单中输入的用户名是否与有效用户列表匹配。一个问题:变量$usrname
拼写错误(应该是$username
'e')。由于严格检查关闭,Perl 愉快地插入了(未声明的)全局变量的值$usrname
,或undef
。这将看起来无辜的片段变成了这个怪物:
my ($match) = grep { // } @users;
它匹配有效用户列表中的所有内容并返回第一个匹配项。您可以在表单的用户名字段中输入任何您想要的内容,脚本会认为您是有效用户。由于警告也关闭了,因此在开发过程中从未发现过这种情况。当你打开警告时,脚本仍然会运行并返回一个用户,但你也会得到这样的东西:
Name "main::usrname" used only once: possible typo at -e line 1.
Use of uninitialized value $usrname in regexp compilation at -e line 1.
当您还打开 strict 时,脚本无法编译,甚至根本不会运行。这个片段还有其他问题(例如,字符串“a”将匹配用户名“janedoe”),但严格和警告至少提醒我们一个主要问题。我怎么强调都不为过:总是,总是 use strict; use
warnings;
无污点模式
Web 开发的第一条规则是“始终清理用户输入”。在我之后重复:始终清理用户输入。再来一次:始终清理用户输入。
换句话说,永远不要盲目相信用户输入而不先验证它。用户(即使是非恶意用户)非常擅长将创意值输入到表单字段中,这可能会破坏您的应用程序(或更糟)。如果您不限制他们的创造力,那么恶意用户可以对您的站点造成的损害是没有限制的(请参阅OWASP 前 10 名中的常年 #1 漏洞,注入)。
Perl 的污点模式可以帮助解决这个问题。system()
污点模式强制您在将其用于某些潜在危险操作(如函数)之前检查所有用户输入
。污染模式就像枪上的安全装置:它可以防止很多痛苦的事故(尽管如果你真的想在脚上开枪,你可以随时关闭安全装置,例如当你在没有实际删除危险字符的情况下取消污染变量时)。在您编写的每个 CGI 脚本中打开污点模式。您可以通过传递-T
标志来启用它,如下所示:
#!/usr/bin/perl -T
启用污点模式后,如果您尝试在危险情况下使用受污染的数据,您的脚本将引发致命错误。这是我在互联网上的随机脚本中发现的这种危险情况的示例:
open(LOCAL, ">/home/Desktop/$name") or die $!;
好吧,我撒谎了,该片段不是来自随机脚本,而是来自您的代码。单独来看,这个片段只是乞求遭受目录遍历攻击,恶意用户输入相对路径以访问他们不应该访问的文件。
幸运的是,您在这里做了一些事情:您通过使用正则表达式*确保不$name
包含目录分隔符。这正是污点模式需要你做的。污点模式的好处是,如果您忘记清理输入,您将立即收到如下错误警报:
Insecure dependency in open while running with -T switch at foo.cgi line 5
与严格一样,污点模式迫使您立即通过导致程序失败来解决代码中的问题,而不是让它静静地跛行。
* 你做对了一些事情,但也做错了一些事情:
- 如果用户只传入一个没有目录分隔符的文件名,你的程序就会死掉,例如
foo
- 您不会删除可以由 shell 解释的特殊字符,例如
|
- 您永远不会清理变量
$file
,但您稍后会尝试在代码中使用它来读取文件
- 您不检查您正在写入的文件是否已经存在(请参阅下面的“不检查文件是否存在”)
- 您允许用户选择将存储在您的服务器上的文件的名称,这给了他们比您应该熟悉的更多的控制权(请参阅下面的“允许用户设置文件名”)
CGI::Carp fatalsToBrowser
由于您仍在测试您的脚本,因此我将为您提供对这个问题的怀疑的好处,但以防万一您不知道并且由于我已经在谈论 CGI 安全问题,请永远不要启用 CGI::Carp 的 fatalsToBrowser生产环境中的选项。它可以向攻击者透露有关脚本内部工作的私密细节。
两个参数open()
和全局文件句柄
两个参数open()
,例如
open FH, ">$file"
当允许用户指定文件路径时,它会带来许多安全风险。您的脚本通过使用硬编码的目录前缀来缓解其中的许多问题,但这绝不会减少使用两个参数 open 可能非常危险的事实。通常,您应该使用三参数形式:
open my $fh, ">", $file
(如果您允许用户指定文件名,这仍然很危险;请参阅下面的“允许用户设置文件名”)。
另请注意,FH
我切换到了词法文件句柄,而不是全局文件句柄$fh
。出于某些原因,请参阅 CERT 的页面不要使用裸字文件句柄。
不检查文件是否存在
/home/Desktop/$name
当您打开文件进行写入时,您不会检查文件是否已经存在。如果该文件已经存在,您将在调用成功后立即open()
截断它(删除其内容) ,即使您从未向该文件写入任何内容。用户(恶意的和其他的)可能会破坏彼此的文件,这并不能建立一个非常满意的用户群。
文件大小没有限制
“但是等等,”你说,“我MAX_FILE_SIZE
在我的 HTML 表单中设置了!” 了解这只是对浏览器的建议;攻击者可以轻松地编辑 HTTP 请求以消除这种情况。永远不要依赖隐藏的 HTML 字段来保证安全。隐藏字段在页面的 HTML 源代码和原始 HTTP 请求中清晰可见。您必须限制服务器端的最大请求大小,以防止用户将大量文件加载到您的服务器并帮助缓解一种类型的拒绝服务攻击。$CGI::POST_MAX
在 CGI 脚本的开头设置变量,如下所示:
$CGI::POST_MAX=1024 * 30; # 30KB
或者更好的是,在您的系统上找到 CGI.pm 并更改 的值,$POST_MAX
以便为所有使用 CGI 模块的脚本全局设置它。这样您就不必记住在您编写的每个 CGI 脚本的开头设置变量。
CGI 与 HTML 表单不匹配
您在 HTML 表单中用于文件路径的 POST 变量userfile
与您在 CGI 脚本中查找的变量file
. 这就是您的脚本因错误而失败的原因
Is a directory
的价值
$cgi->param('file')
是undef
这样你的脚本试图打开路径
/home/Desktop/
作为常规文件。
处理上传的过时方法
您正在使用使用 CGI.pm 处理上传的旧(和过时)方法,其中param()
用于获取文件名和轻量级文件句柄。这不适用于严格并且不安全。该upload()
方法是在 v2.47 中添加的(早在 1999 年!)作为首选替代方法。像这样使用它(直接来自CGI.pm 的文档):
$lightweight_fh = $q->upload('field_name');
# undef may be returned if it's not a valid file handle
if (defined $lightweight_fh) {
# Upgrade the handle to one compatible with IO::Handle:
my $io_handle = $lightweight_fh->handle;
open (OUTFILE,'>>','/usr/local/web/users/feedback');
while ($bytesread = $io_handle->read($buffer,1024)) {
print OUTFILE $buffer;
}
}
wherefield_name
是保存文件名的 POST 变量的名称(在您的情况下为userfile
)。请注意,示例代码没有根据用户输入设置输出文件名,这导致了我的下一点。
允许用户设置文件名
永远不要让用户选择将在您的服务器上使用的文件名。如果攻击者可以将恶意文件上传到已知位置,他们就更容易被利用。相反,生成一个新的、唯一的(以防止破坏)、难以猜测的文件名,最好是在您的 Web 根目录之外的路径中,这样用户就无法通过 URL 直接访问它们。
其他问题
您甚至还没有开始解决以下问题。
验证
谁可以使用您的网络应用上传文件?您将如何确保只有授权用户才能上传文件?
访问控制
是否允许用户查看其他用户上传的文件?根据文件内容,可能存在重大的隐私问题。
上传数量和速率
一个用户可以上传多少个文件?一个用户在固定时间内允许上传多少个文件?如果您不限制这些,即使您强制执行最大文件大小,一个用户也很容易很快耗尽您的所有服务器资源。
危险文件类型
您将如何检查用户是否没有将危险内容(例如,可执行的 PHP 代码)上传到您的服务器?仅仅检查文件扩展名或内容类型标题是不够的;攻击者已经找到了一些非常有创意的方法来规避此类检查。
“但是,但是,我只是在我的公司内部网上运行这个......”
如果您的脚本无法从 Internet 访问,您可能会倾向于忽略这些安全问题。但是,您仍然需要考虑
- 办公室恶作剧
- 心怀不满的同事
- 需要访问您的应用程序或不应访问您的应用程序的合作者和外部承包商
- 非常喜欢您的应用程序的经理,他们决定在您不知情的情况下将其开放给互联网上的用户,可能是在您转移到另一个小组或离开公司之后
“我该怎么办?”
废弃现有的代码。仔细阅读我在第一段中列出的资源。他们又来了:
如果您真的需要这样做,请仔细考虑。如果您只需要给用户一个存储文件的地方,请考虑改用 (S)FTP。这当然不会消除所有安全风险,但会消除一个大风险:您的自定义 CGI 代码。
如果经过仔细考虑后您仍然认为这是必要的,请阅读一些最近的Perl 教程,以确保您可以使用和理解现代 Perl 编程约定。使用Catalyst、Dancer或Mojolicious等框架代替 CGI.pm,所有这些都有可以处理棘手领域的插件,例如用户身份验证和会话,因此您不必重新发明轮子(很糟糕)。
遵循上述资源中列出的所有安全指南,并考虑寻求网络安全专家的帮助。小心行事:您的代码中的一个错误可能会让攻击者破坏您的整个站点,甚至可能破坏您网络上的其他机器。根据您的公司和用户所在的国家/地区,这甚至可能产生法律后果。
</肥皂盒>