TL;DR
停止。立即搁置你的脚本。这是一个巨大的安全漏洞,等待被利用。阅读以下资源:
当您阅读并理解所有这些内容后,请停下来思考一下您是否真的需要让用户将文件上传到您的服务器上。想想long和hard。你真的能解释所有列出的漏洞吗?如果您仍然觉得需要这样做,请考虑寻求安全专家的帮助。 仔细遵循上述资源中列出的指南,并了解设计中的错误可能会危及整个网站。
我知道这只是一个测试脚本,而不是生产应用程序(至少,我真的希望是这样),但即便如此,你正在做什么(尤其是如何你这样做)是一个非常、非常 坏主意。这里有一个选择几个
原因,来自Unrestricted File
Upload上的OWASP页面:
- 网站可以被污损。
- 可以通过上传和执行 web-shell 来入侵 web 服务器,该 web-shell 可以:运行命令、浏览系统文件、浏览本地资源、攻击其他服务器以及利用本地漏洞等等。
- 此漏洞可能使网站容易受到 XSS 等其他类型的攻击。
- 可以通过将恶意文件上传到服务器来利用本地文件包含漏洞。
来自 OWASP 的更多信息:
上传的文件会给应用程序带来重大风险。进入的第一步
许多攻击是获取一些代码到系统中被攻击。然后攻击
只需要找到一种方法来执行代码。使用文件上传有帮助
攻击者完成了第一步。
不受限制的文件上传的后果可能会有所不同,包括完全
系统接管,文件系统过载,将攻击转发到后端
系统和简单的污损。
很可怕的东西,嗯?
问题
您的代码
让我们先来看看您发布的代码存在的一些问题。
没有严格,没有警告
开始将use strict; use warnings; 放在你的每个Perl 脚本的顶部
曾经写过。 我最近有幸修复了一个 CGI 脚本,其中包含
一个 sn-p 是这样的:
my ($match) = grep { /$usrname/ } @users;
此代码用于检查在 HTML 表单中输入的用户名是否与
有效用户列表。一个问题:变量$usrname 是
拼写错误(应该是 $username 并带有“e”)。由于严格
检查已关闭,Perl 愉快地插入了(未声明的)全局的值
变量$usrname,或undef。这将看似无辜的 sn-p 变成了这个怪物:
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 时,脚本无法编译,甚至不会在
全部。这个 sn-p 还有其他问题(例如,字符串 'a' 将匹配用户名 'janedoe'),但严格和警告至少提醒了我们一个主要问题。我不能强调这一点:总是,总是 use strict; use
warnings;
无污染模式
Web 开发的第一条规则是,
“始终清理用户输入。”在我之后重复:始终清理用户输入。再来一次:始终清理用户输入。
换句话说,从不
盲目相信用户输入而不首先验证它。用户(即使是非恶意用户)非常擅长将创意值输入表单
可能会破坏您的应用程序(或更糟)的字段。如果你不限制他们的创造力,
恶意用户可以对您的网站造成的破坏没有限制(请参阅perennial #1
vulnerability on the OWASP Top 10,
injection)。
Perl 的污点模式可以帮助解决这个问题。污染模式迫使你
在某些潜在危险操作中使用之前检查所有用户输入,例如
system() 函数。污染模式就像枪上的保险:它可以防止很多痛苦
意外(虽然如果你真的想在脚上开枪,你可以
始终关闭安全性,例如当您在未实际删除危险字符的情况下清除变量时)。
在您编写的每个 CGI 脚本中打开污染模式。您可以通过传递 -T 标志来启用它,如下所示:
#!/usr/bin/perl -T
启用污点模式后,如果您尝试
在危险情况下使用受污染的数据。这是我在互联网上的随机脚本中发现的这种危险情况的示例:
open(LOCAL, ">/home/Desktop/$name") or die $!;
好吧,我撒谎了,sn-p 不是来自随机脚本,而是来自你的代码。单独来看,这个 sn-p 只是乞求被directory traversal attack 击中,恶意用户输入相对路径以访问他们不应该访问的文件。
幸运的是,您在这里做了一些事情:您通过使用正则表达式* 确保了$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"
当用户被允许指定文件路径时有一个host of security risks associated with it。您的脚本通过使用硬编码的目录前缀来缓解其中的许多问题,但这绝不会减少使用两个参数 open 可能非常危险的事实。 通常,您应该使用三个-论证形式:
open my $fh, ">", $file
(如果您允许用户指定文件名,这仍然很危险;请参阅下面的“允许用户设置文件名”)。
还要注意,我切换到了一个词法文件句柄 $fh,而不是全局文件句柄 FH。出于某些原因,请参阅 CERT 的页面 Do not use bareword filehandles。
不检查文件是否存在
当您打开文件进行写入时,您不会检查文件是否已存在于/home/Desktop/$name。如果文件已经存在,您将在open() 调用成功后立即截断它(删除其内容),即使您从未向该文件写入任何内容。用户(恶意用户和其他用户)很可能会破坏彼此的文件,这并不能建立一个非常满意的用户群。
文件大小无限制
“但是等等,”你说,“我在我的 HTML 表单中设置了MAX_FILE_SIZE!”了解这只是对浏览器的建议;攻击者可以轻松地编辑 HTTP 请求以消除这种情况。 永远不要依赖隐藏的 HTML 字段来保证安全性。 隐藏的字段在您页面的 HTML 源代码和原始 HTTP 请求中清晰可见。您必须限制 服务器端 的最大请求大小,以防止用户将大量文件加载到您的服务器并帮助缓解一种类型的拒绝服务攻击。在 CGI 脚本的开头设置 $CGI::POST_MAX 变量,如下所示:
$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 年!)作为首选替代方法。像这样使用它(直接从documentation for 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;
}
}
其中field_name 是保存文件名的POST 变量的名称(在您的情况下为userfile)。请注意,示例代码没有根据用户输入设置输出文件名,这导致了我的下一点。
允许用户设置文件名
从不允许用户选择将在您的服务器上使用的文件名。如果攻击者可以将恶意文件上传到已知位置,他们就更容易被利用。相反,生成一个新的、唯一的(以防止破坏)、难以猜测的文件名,最好在您的 Web 根目录之外的路径中,这样用户就无法通过 URL 直接访问它们。
其他问题
您甚至还没有开始解决以下问题。
认证
谁可以使用您的网络应用上传文件?您将如何确保只有授权用户才能上传文件?
访问控制
是否允许用户查看其他用户上传的文件?根据文件内容,可能存在重大的隐私问题。
上传次数和上传率
一个用户可以上传多少个文件?一个用户在固定时间内允许上传多少个文件?如果您不限制这些,即使您强制设置最大文件大小,一个用户也很容易很快耗尽您的所有服务器资源。
危险文件类型
您将如何检查用户是否没有将危险内容(例如,可执行的 PHP 代码)上传到您的服务器?仅仅检查文件扩展名或内容类型标题是不够的;攻击者发现了一些非常有创意的方法来规避此类检查。
“但是,但是,我只在我的公司 Intranet 上运行它...”
如果您的脚本无法从 Internet 访问,您可能会倾向于忽略这些安全问题。不过,你还是需要考虑
- 办公室恶作剧
- 心怀不满的同事
- 需要访问您的应用或不应访问您的应用的合作者和外部承包商
- 非常喜欢您的应用的经理们决定在您不知情的情况下将其开放给互联网上的用户,可能是在您转移到另一个小组或离开公司之后
“我该怎么办?”
废弃现有代码。仔细阅读我在第一段中列出的资源。他们又来了:
如果您真的需要这样做,请仔细考虑。如果您只需要给用户一个存储文件的地方,请考虑改用 (S)FTP。这当然不会消除所有安全风险,但会消除一个大风险:您的自定义 CGI 代码。
如果经过仔细考虑后您仍然认为这是必要的,请通过一些recent Perl tutorials 来确保您可以使用和理解现代 Perl 编程约定。使用 Catalyst、Dancer 或 Mojolicious 之类的框架来代替 CGI.pm,所有这些都有可以处理棘手领域(如用户身份验证和会话)的插件,因此您不必重新发明轮子(差)。
遵循上述资源中列出的所有安全准则,并考虑寻求网络安全专家的帮助。小心行事:您的代码中的一个错误可能会让攻击者破坏您的整个站点,甚至可能破坏您网络上的其他计算机。根据您的公司和用户所在的国家/地区,这甚至可能产生法律后果。