4.2 输入处理与安全性
本节专门讨论 Web 应用中对“输入值”的处理,以及输入处理在安全性策略中的地位。虽然校验输入值本身并不是安全性策略,但是,在安全性对策存在缺陷的情况下,通过校验输入值能够防止危害的发生,或者减轻损害的程度。
什么是 Web 应用的输入处理
Web 应用中的输入即由 HTTP 请求传入的信息,比如 GET、POST、Cookie 等。Web 应用接收到这些值时所做的处理,在本书中称为“输入处理”。以图 4-2 所示的“输入-处理-输出”模型为例,在这一模型中,Web 应用的输入处理即为业务逻辑处理之前的数据准备阶段。
图 4-2 “输入-处理-输出”模型
输入处理就是指对输入值做如下处理。
(a)检验字符编码的有效性 3
3 本章不深入讲解字符编码。详情请参考第 6 章。
(b)必要时转换字符编码
(c)检验参数字符串的有效性
之所以检验字符编码的有效性是因为存在利用字符编码的攻击手段 4 (参考第 6 章)。虽然理论上只要确保所有使用字符串的地方都能正确处理字符编码就不会有问题,但现实中由于编程语言的漏洞或者写代码时的疏忽,安全隐患却防不胜防。而另一方面,如果 Web 应用能够将字符编码不正确的数据拒之门外,就能抵御利用非法字符编码发动的攻击。
4 此外,因为程序的正常运行也要求字符编码没有问题,所以检验字符编码有效性也是为了保证程序的正常运行。
(b)处理中的转换字符编码,指的是在 HTTP 消息与程序内部使用的字符编码不一致的情况下需要进行的处理。
(c)处理中的校验输入值,与其说是安全性方面的要求,不如说是依据应用软件规格执行的操作,但不管怎样,从结果上来看确实对提升应用的安全性起到了辅助作用。
下面让我们来分别看一下上述 3 点的详细内容。
检验字符编码
PHP 中能使用 mb_check_encoding
函数检验字符编码。
格式清单 mb_check_encoding 函数
bool mb_check_encoding(string $var, string $encoding)
第一个参数 $var
是检验对象字符串,第二个参数 $encoding
是字符编码。 $encoding
可以省略,省略时函数使用 PHP 的内部字符编码。如果 $var
字符串的字符编码正确则函数返回 true。
其他编程语言中检验字符编码的方法请参考第 6 章。
转换字符编码
转换字符编码的方法因编程语言而异。总体上可分为自动转换字符编码的语言和在脚本中手动转换字符编码的语言。PHP 中通过设置 php.ini 文件,可切换上述两种方式。
表 4-2 主流 Web 开发语言中提供的转换字符编码的方法
语言 | 自动转换 | 手动转换 |
---|---|---|
PHP | php.ini 等 | mb_convert_encoding |
Perl | × | Encode::decode |
Java | setCharacterEncoding | String 类 |
ASP.NET | Web.config | × |
表 4-2 中归纳了主流 Web 开发语言中提供的转换字符编码的方法。接下来、让我们以手动转换字符编码的方式为例进行说明。
PHP 中使用 mb_convert_encoding
函数来手动转换字符编码。
格式清单 mb_convert_encoding 函数
string mb_convert_encoding(string $str, string $to_encoding, string $from_encoding)
mb_convert_encoding
函数的 3 个参数分别为:转换前的字符串、转换后的字符编码、转换前的字符编码。返回值为转换后的字符串。
检验并转换字符编码的实例
这里我们来看一个检验并转换字符编码的实例。以下 PHP 脚本表示的是接收 Shift_JIS 编码的文字列 name
后将其显示在页面上。脚本的内部编码为 UTF-8,所以需要使用 mb_convert_encoding
函数来转换文字编码。
代码清单 /42/42-001.php
<?php
$name = isset($_GET['name']) ? $_GET['name'] : '';
// 校验字符编码(Shift_JIS)
if (! mb_check_encoding($name, 'Shift_JIS')) {
die(' 字符编码有误 ');
}
// 转换字符编码(Shift_JIS → UTF-8)
$name = mb_convert_encoding($name, 'UTF-8', 'Shift_JIS');
?>
<body>
名字为 <?php echo htmlspecialchars($name, ENT_NOQUOTES, 'UTF-8'); ?>
</body>
正常情况下执行结果如图 4-3 所示。
图 4-3 42-001.php 执行结果示例(正常情况)
下图为使用不符合 Shift_JIS 编码的字符串 %82%21 时的页面显示。由于 Shift_JIS 双字节的第二个字节必须是 0x40 以上的值,而 %20 不符合要求,所以是无效的 Shift_JIS 数据。相关详情可参考第 6 章。
图 4-4 输入值不符合 Shift_JIS 编码
专栏:字符编码的自动转换与安全性
前面提到 PHP 中能通过编辑 php.ini 使字符编码自动转换。其实,也有些编程语言,如 Java 与 .NET,主要使用自动转换字符编码。
转换字符编码时,不正确的字符会被删除或被替换为其他字符(
?
或者 Unicode 的替换字符 U+FFFD),因此,即便是自动转换字符,也能够防御利用字符编码的攻击手段。虽然使用自动转换能让写代码的工程轻松不少,但也存在以下缺点。
- 用户没有注意到程序内部出现了文字乱码而继续操作
- 迁移服务器等导致 php.ini 文件被改变后,会有丢失校验操作的风险
因此,本书专门介绍了检验字符编码和手动转换字符编码的方法。
至于如何选择自动转换与手动转换,需要在了解两者各自的优缺点的前提下,在开发团队中达成统一认识,或者在进行各个项目时分别选择。
输入校验
处理完字符编码的相关操作后,就进入到了输入校验的阶段。下面,让我们首先了解一下 Web 应用中输入校验的概要,并在此基础上探讨输入校验与安全性的关系。
- 输入校验的目的
为了理解输入校验的目的,我们首先来看一下没有对输入值进行校验的 Web 应用的情况。如果 Web 应用没有对输入值进行校验的话,或许就会出现以下现象。
- 用户在只接受数值的项目中填入了字母或标点符号,导致保存至数据库时发生错误
- 更新处理时中途发生错误,导致数据库的不一致性
- 用户填写完很多项目后点击确认按钮时因发生了内部错误而不得不全部重新填写
- 程序在用户漏填邮箱地址的情况下依然执行发送邮件的处理
像这样,不校验输入值会导致应用程序内部业务逻辑在中途发生错误,以及乍一看似乎很正常,但相关操作在后台其实根本没有被处理,或者没有被处理完等问题。
而输入校验就是为了减少此类事故的发生。然而,输入校验说到底也只是对字符串的格式进行检查,格式以外的其他条件(如是否还有库存,账户余额是否充足等)则并不会得到检验。因此,输入校验并不能消灭所有的错误,但是通过尽早通知用户输入存在不妥并让其改正,可以使应用的易用性得到提高。
综上,输入校验的目的可以被总结如下。
- 尽早发现输入错误并提示用户重新输入,提高了易用性
- 防止错误处理造成数据不一致等,提高了系统的可靠性
- 输入校验与安全性
虽然输入校验的主要目的并不是安全性,但有时却也能对提高应用的安全性大有裨益。例如以下情况。
- 在有些参数忘了防范 SQL 注入攻击的情况下,因为输入检验时只允许字母和数字,所以就能避免损害
- 在 PHP 中使用了非二进制安全的函数(稍后讲述)的情况下,因为输入校验时过滤了控制字符,所以就能避免损害
- 在页面显示处理函数中对字符编码的指定有所疏忽时,因为输入校验时检验了字符编码的有效性,所以就能避免损害 5
- 二进制安全与空字节攻击
刚才出现了“二进制安全”这个用语。二进制安全是指,不管输入值是怎样的字节列都能将其原封不动地进行处理的功能,特别是当包含零值字节(NULL 字节,PHP 中记为 \0)时也能正确处理。
空字节之所以特殊,是因为在 C 语言以及 Unix 与 Windows 的 API 中规定空字节为字符串的结尾。因此,底层为 C 语言的 PHP 以及其他脚本语言中,有些函数不能正确处理空字节。而这类函数就被称为不是二进制安全的函数。
利用空字节的攻击手段被称为空字节攻击。空字节攻击本身并不造成伤害,而是通常被用于绕过其他安全隐患的防范策略。
以下为没有进行空字节攻击防范的范例脚本。42-002.php 使用正则表达式
ereg
检验变量$p
的值中仅包含数字。代码清单 /42/42-002.php
<body> <?php $p = $_GET['p']; if (ereg('^[0-9]+$', $p) === FALSE) { die(' 请输入整数值 '); } echo $p; ?> </body>
$p
只包含数字的情况下,页面显示应该一切正常。接下来,我们来尝试用如下 URL 来执行 42-002.php。http://example.jp/42/42-002.php?p=1%00<script>alert('XSS')</script>
运行结果如图 4-5 所示。
图 4-5 绕过 ereg 检验的安全隐患
浏览器中执行了 JavaScript 代码,弹出“XSS”的对话框,这就是跨站脚本漏洞(XSS),具体内容将在 4.3 节中详述,但在此也可以看出使用
ereg
的检验是能被绕过的。- ereg 检验被绕过的原因
ereg
检验被绕过的原因是 URL 中含有 %00。%00 就是空字节,由于ereg
函数不是二进制安全的函数,因此,检验对象字符串中如果含有空字节,就会被视作字符串的结束(图 4-6)。图 4-6 空字节攻击
由于
<script>…
以后的字符串被检验函数忽略,检验对象字符串变成了“1”,满足“仅限数字”的要求,因此便通过了检验。而 JavaScript 得以被执行的原因也就是如此。前面已经说过,空字节攻击很少能独自造成损害,而是通常被用来在其他安全隐患防范策略的疏漏中见缝插针。而除了 XSS 外,常与空字节攻击组合使用的还有目录遍历攻击(参考 4.10.1 节)。
虽然在应用中全部使用二进制安全的函数就能完全防御空字节攻击,但实际实现起来却颇为困难。因为很多情况下函数的参考文档中都没有明确记载该函数是否二进制安全。因此,行之有效的策略为,在应用程序的入口处使用二进制安全的函数检验输入值中是否有空字节,如果含有空字节就报错。
- ereg 检验被绕过的原因
- 仅校验输入值并不是安全性策略
至此,读者们或许会产生以下疑问:如果在输入阶段就将所有的非法输入值过滤掉,是不是就能确保应用的安全性了呢?而且在输入阶段就将安全隐患全部搞定的话,之后的工作也更轻松了呢。
但遗憾的是这并不可行。因为输入阶段实施的校验并不能成为安全隐患的防范策略。输入校验是根据应用程序的软件规格而实施的操作。例如,假设规格书中规定允许输入任何字符,那么,在输入阶段就无法进行任何安全性防范措施。
因此,输入校验的作用最多也就是为安全机制多加一层保障。
- 输入校验的依据是应用程序的规格
输入校验时的基准是应用程序的规格。例如电话号码应该全部是数字、用户 ID 应该是 8 位的字母或数字等,各参数允许的字符种类以及长度都应根据应用的要求规格进行设置。
- 校验控制字符
刚才已经提到输入校验的基准是应用程序的规格,但为了在应用的规格中规定“允许输入任何字符”的情况下也能够进行验证,就需要校验控制字符。
控制字符是指,换行符(CR 和 LF)和 Tab 等通常不显示在页面上的、ASCII 编码中 0x20 以下以及 0x7F(DELETE)的字符。前面讲到的空字节也是控制字符。由于 Web 应用中的输入参数多为文本格式,所以应当限制控制字符的输入,然而也有一些 Web 应用未对控制字符进行处理。
单行的文本输入框(input 要素的 type 属性值为 text 或 password)中,由于按常规的输入方法无法输入控制字符,因此多数情况下所有的控制字符都会遭到拒绝。textarea 元素中能够输入换行和 Tab,但是否允许 Tab 则要由规格决定。
- 校验字符数
应用程序的规格文档中应当明确定义所有输入项目的最大字符数。如果是要保存到数据库的值,最大字符数理应与表字段的最大字符数一致。而即使有些输入项目没有物理上的上限值限制,为了保证程序的正常运行,也同样需要确定最大字符数。
某些情况下,校验最大字符数能使应用的安全性更为稳固。由于攻击 Web 应用有时需要用到很长的字符串,因此,假设限制字符串的最大长度为 10 的话,那么就能使攻击者在发现 SQL 注入隐患时也无法实施攻击。虽然我们不能对校验字符数的效果抱有过多期待,但也应该认识到校验字符数的必要性以及其对安全性的帮助。
- 校验控制字符
- 哪些参数需要校验
输入校验的对象为所有的参数。hidden 参数、单选框、select 元素等也不例外。Cookie 中包含会话 ID 以外的值的情况下,Cookie 值也是校验对象。此外,应用中用到了 Referer 等 HTTP 消息头时也需要进行校验。
- PHP 的正则表达式库
利用正则表达式能够便利地实现输入校验。PHP 中可以利用的正则表达函数有
ereg
、preg
、mb_ereg3
大类。其中,ereg
由于不是二进制安全的,因此在 PHP5.3 及以后的版本中已被废弃,而改用了preg
或mb_ereg
。preg
仅在字符编码为 UTF-8 的情况下能正常处理中文字符,而mb_ereg
则适用于大多数字符编码。通过在程序的开头使用
preg
或mb_ereg
进行包含空字节的控制字符校验,就能够同时进行应用规格中的字符种类校验和空字节校验。关于正则表达式的详情请参考 PHP 的文档或说明手册。下面,我们通过具体例子来了解一下 PHP 中输入校验时的注意点。
- 使用正则表达式检验输入值的实例(1)1 ~ 5 个字符的字母数字
下面的代码展示了使用
preg_match
函数来校验“1-5 个字符的字母数字”的范例。代码清单 /42/42-010.php
<?php $p = isset($_GET['p']) ? $_GET['p'] : ''; if (preg_match('/\A[a-z0-9]{1,5}\z/ui', $p) == 0) { die(' 请输入 1-5 个字符长度的字母或数字 '); } ?> <body> p 的值为 <?php echo htmlspecialchars($p, ENT_NOQUOTES, 'UTF-8'); ?> </body>
传递给
preg_match
的正则表达式可以按照图 4-7 这样进行解释。图 4-7 检验“1~5 个字符的字母数字”的正则表达式
其中,各部分的意思分别如下。
- u 修饰符
在中文环境中使用
preg_match
函数时,无论检验对象是否含有中文,都必须指定表示字符编码为 UTF-8 的u
修饰符。 - i 修饰符
i
修饰符表示匹配时不区分大小写。 - 全体一致匹配时使用 \A 和 \z
\A
代表数据的开头,\z
代表数据的结尾。有时也会使用^
和$
来代替\A
和\z
,但由于^
和$
代表“行的”开头和结尾,$
会匹配换行符,所以当它们被用于匹配数据的开头和结尾时就有可能产生 Bug。图 4-8 的脚本中使用了
^
和$
代替\A
和\z
6 ,校验对象字符的结尾处为%0a
(LF 换行)。能看到换行符绕过了校验。图 4-8 换行符绕过了校验
- 字符集合
[
和]
围住的部分就是字符集合。在方括号内将允许的字符全部列举出来,或者使用[0-9]
这样的形式来指定范围。指定字母,可使用[a-zA-Z]
。指定字母与数字,可使用[a-zA-Z0-9]
。而使用i
修饰符后,只需在大写文字与小写文字中任选其一即可。 - 数量修饰符
{
和}
围住的部分就是数量修饰符。{1,5}
的意思是字符数大于等于 1 且小于等于 5。允许为空(0 字符)的情况下指定为{0,5}
。 - 使用 mb_ereg
如果不用
preg_match
而改用mb_ereg
函数,就需要将脚本的开头部分作如下修改。代码清单 /42/42-012.php(选摘)
<?php // mb_regex_encoding 在设置了内部编码的情况下可以省略 mb_regex_encoding('UTF-8'); // 只要在程序开头设置一次即可 $p = isset($_GET['p']) ? $_GET['p'] : ''; if (mb_ereg('\A[a-zA-Z0-9]{1,5}\z', $p) === false) { die(' 请输入 1-5 个字符长度的字母或数字 '); } ?>
mb_regex_encoding
函数的作用为指定mb_ereg
函数的字符编码。如果 php.ini 已经设置了内部字符编码,此步骤可以省略。mb_ereg
与preg_match
的不同之处有 3 点:mb_ereg
的正则表达式不需要用/
括起来;不使用u
修饰符;没找到匹配项时返回 false。另外,由于m b_ereg
的返回值为整数或布尔型,所以比较时应使用区分类型的===
运算符。
- u 修饰符
- 使用正则表达式检验输入值的实例(2)住址栏
住址和姓名等的输入框多数情况下只限制字符的长度而不限制字符的种类。但是,即使不限制字符种类,也应当检查是否有控制字符混入,以防范空字节攻击。例如,下面脚本的正则表达式中就使用了 POSIX 字符集合 7
[[:^cntrl:]]
来表示“非控制字符的字符”。代码清单 /42/42-013.php
<?php $addr = isset($_GET['addr']) ? $_GET['addr'] : ''; if (preg_match('/\A[[:^cntrl:]]{1,30}\z/u', $addr) == 0) { die(' 请输入长度小于 30 个字符的地址(必填项)。不能使用换行或 Tab 等控制字符 '); }
输入评论等使用的 textarea 元素(多行输入文本框)中允许包含控制字符中的换行(有时也允许 Tab),这种情况下可以使用如下正则表达式。下例的意思是,禁止除换行和 Tab 以外的控制字符,字符长度为 1~400。
preg_match('/\A[\r\n\t[:^cntrl:]]{1,400}\z/u', $comment)
专栏:请注意 mb_ereg 中的 \d 与 \w
正则表达式内置了一些字符集合,如
\d
匹配数字,\w
匹配英文字母、数字和下划线。但是,mb_ereg 中使用\d
或\w
时也能匹配全角字符。比如\d
就能够匹配全角数字(仅限 Unicode)。虽然全角数字也是数字这一解释在个别情况下会有所帮助,但是,Web 应用中常见的数值校验中是不允许对象为全角数字的。
像这样,使用内置的字符集合可能会匹配到预想以外的结果,因此,安全起见,建议使用
[a-zA-Z0-9_]
这类明确声明字符集合的方式。
5 详情可参考后续章节中对跨站脚本和 SQL 注入的讲解,以及第 6 章。
6 本书支持页面中的 /42/42-011.php。
7 POSIX 是 IEEE 规定的基于 Unix 操作系统的共通规格,其中也包含了正则表达式的规格。POSIX 字符集合则是指 POSIX 正则表达式中定义的字符集合。
范例
作为以上内容的总结,接下来我们来看一个 PHP 脚本范例,该脚本的目的在于接收 URL 中的查询字符串 name
并将其显示在页面上。
代码清单 /42/42-020.php
<?php
// 取得参数后校验并转换字符编码
// 同时执行了输入值校验的函数
// $key : GET 参数名
// $pattern : 用于验证输入值的正则表达式字符串
// $error : 验证输入值时的错误消息
// 返回值 : 取得的参数(string)
function getParam($key, $pattern, $error) {
$val = isset($_GET[$key]) ? $_GET[$key] : '';
// 校验字符编码(Shift_JIS)
if (! mb_check_encoding($val, 'Shift_JIS')) {
die(' 字符编码有误 ');
}
// 转换字符编码(Shift_JIS → UTF-8)
$val = mb_convert_encoding($val, 'UTF-8', 'Shift_JIS');
if (preg_match($pattern, $val) == 0) {
die($error);
}
return $val;
}
// 调用取得参数的函数
$name = getParam('name', '/\A[[:^cntrl:]]{1,20}\z/',
' 请输入长度小于 20 个字符的姓名(必填项)。不能使用控制字符 ');
?>
<body>
姓名为 <?php echo htmlspecialchars($name, ENT_NOQUOTES, 'UTF-8'); ?>
</body>
getParam
函数中进行了读取字符串、校验字符编码、转换字符编码、输入校验等操作。定义此类能够复用的共通方法,能够使后续的开发过程轻松很多。
范例代码中也存在一些不足之处,比如错误消息过于简陋难以理解等。这里笔者希望将代码的改善工作作为习题留给读者。
专栏:输入校验与框架
前面介绍了在应用程序中通过业务逻辑来进行输入校验的方法,而在使用 Web 应用开发框架的情况下,也能利用框架中提供的输入校验功能,从而简化开发流程。
以微软的 .NET Framework 为例,该框架提供了名为“校验控件”的可视化输入校验功能。图 4-9 展示了在 Visual Web Developer 2010 中使用 RangeValidator 校验控件的情形。RangeValidator 能校验输入值的类型与长度范围,可以看出本例中的输入值为 Integer 型,长度范围为 1~100 字符。具体内容请查看图 4-9 中的
Type
、MinimumValue
、MaximumValue
、ErrorMessage
这些属性。图 4-9 校验控件中的属性设置
图 4-10 为运行后的页面。
图 4-10 RangeValidator 的运行示例
截图是输入“101”后将输入焦点移动时的情形。通过 JavaScript 的检验在页面上显示了“请输入范围为 1~100 的整数”的消息。这是在 RangeValidator 的
ErrorMessage
属性中设置的消息。同样的校验也会在服务器端执行。除 .NET Framework 之外,很多其他的开发框架同样也提供了输入校验的功能,在实际进行开发工作时,可根据情况对其善加利用。
总结
在 Web 应用的入口处,程序会执行以下三类操作,即检验输入字符编码、转换字符编码、输入校验。虽然这些操作并非根本性的安全性策略,但也能够有助于对框架和应用中潜在的安全隐患进行防范。
- 输入校验的依据是应用的规格
- 检验字符编码
- 检验包含控制字符在内的字符种类
- 检验字符数
实施流程如下。
- 设计阶段将各个参数的字符种类以及最大字符数写入软件规格说明书。
- 设计阶段决定输入校验的实现方针。
- 开发阶段依照设计阶段的决定实现输入校验。
参考:表示“非控制字符的字符”的正则表达式
作为参考,此处介绍一下 PHP、Perl、Java、VB.NET 中表示“非控制字符的字符”的正则表达式。下面各例子的目的都是确认“输入值为 0~100 个字符且不包含控制字符”。
- PHP(preg_match)
以下为使用 POSIX 字符集合的例子。
if (preg_match('/\A[[:^cntrl:]]{0,100}\z/u', $s) == 1) { # 输入校验 OK
PHP 的
preg_match
函数除了使用 POSIX 字符集合外,还能使用 Perl 风格的\P{Cc}
。这种写法也适用于 Perl、Java、.NET 等语言。if (preg_match('/\A\P{Cc}{0,100}\z/u', $s) == 1) { # 输入校验 OK
- PHP(mb_ereg)
mb_ereg
只能使用 POSIX 字符集合。if (mb_ereg('\A[[:^cntrl:]]{0,100}\z', $addr) !== false) { # 输入校验 OK
- Perl
Perl 能使用
\P{Cc}
来指定控制字符以外的字符。由于 Perl 中能够使用正则表达式字面量,所以不必使用两个\
来转义。if ($s =~ /\A\P{Cc}{0,100}\z/) { # 输入校验 OK
- Java
Java 中可以使用 String 类的
matches
方法。matches
方法匹配规则为全体一致,所以正则表达式中不必使用\A
和\z
。Java 中正则表达式的形式为字符串,所以需要使用两个\
来转义。if (s.matches("\\P{Cc}{0,100}")) { // 输入校验 OK
- VB.NET
.NET Framework 中提供了使用
Regex
类进行正则表达式查询的功能。VB.NET 的字符串字面量中不需要使用两个\
来转义。if Regex.IsMatch(s, "\A\P{Cc}{0,100}\z") then ' 校验 OK
参考文献
[1] 徳丸浩 (.2009 年 6 月 2 日). 主要言語別:入力値検証の具体例~入力に関する対策(3). 参考日期:2011 年 1 月 6 日,参考网址:ITpro: http://itpro.nikkeibp.co.jp/article/COLUMN/20090525/330611/
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论