6.3 字符编码方式
前面我们对什么是对字符集进行了说明,从本节开始我们将对字符集如何在计算机上表示及处理,也就是编码(Encoding)进行说明。首先我们将介绍一下比较常用的日文字符编码方式,然后再针对这些编码方式的特点及注意事项加以说明。
- 什么是编码方式
字符集(符号化字符集)虽然已经被标上了编号,也许我们会觉得直接那样在计算机上使用不就可以了吗?然而实际上没那么简单。首先最初普及的字符集多是单字节字符集,比如 US-ASCII、ISO-8859-1、JIS X 0201 等,后来发展起来的兼容单字节字符集的多字节字符集,比如 JIS X 208 或者 Unicode 等,和 US-ASCII 等单字节字符集还是同时并存的。为了兼容并且同时支持不同的字符集,需要对这些字符集进行编码,这种编码过程就叫作“编码”或者“字符编码方式”。
在日语 Web 应用程序里多使用的字符编码方式有基于 JIS 系列字符集的 Shift_JIS 和 EUC-JP,以及基于 Unicode 字符集的 UTF-16 和 UTF-8。
下面我们分别对这些字符编码方式进行说明。
- Shift_JIS
1980 年代初期,个人计算机开始在日本普及,同时也出现了针对在计算机上处理日语汉字的新字符编码方式的需求。为了满足这种需求,出现了将字符集 JIS X0208 映射到 JIS X0201 空白区域的编码方法,这就是 Shfit_JIS。Shift_JIS 使用的字符集是微软标准字符集,所以 Shift_JIS 也被称为微软字码页(Code Page )932 或者简称为 CP932。
在 Shfit_JIS 编码里,为 2 字节字符中的第一个字节(前置字节)分配了 0x81~0x9F 和 0xE0~0xFC 两块区域,将第二个字节(后置字节)分配给了 0x40~0x7E 和 0x80~0xFC 两块。可以参考图 6-4 里示例的字符编码分配方式。
图 6-4 Shfit_JIS 的各字节分配
和后面要讲到的 EUC-JP 或者 UTF-8 比起来,Shfit_JIS 这种字符编码方式巧妙的使用了第一个字节中比较窄的一部分来扩展出第二字节以容纳更多的汉字,从存储效率上来说是比较高的,但是同时也有一些它固有的缺点。下面我们就来看看它的缺点都有哪些。
- 对字符匹配的影响
第一个缺点是由于 Shift_JIS 编码的第一个字节和第二个字节有部分编码是重合的,如果光把第一个字节拿出来,我们无法分辨它是某一字符编码后的第一个字节的还是第二个字节。
而且,在后置字节的范围里,存在着和第一字节中特殊符号重合的部分,如果对 Shift_JIS 字符处理存在缺漏的话,可能会吧 Shift_JIS 中后置字节的数据误认为特殊符号来处理了。典型例子是由 0x5C“¥”导致的错误,即“5C”问题。
下面我们来看一下具体的例子。首先看一下在字符串“ラリルレロ”中查找字符“宴”的问题,如图 6-5 所示。类似的 Bug,笔者曾经在一个咨询案例中遇到过,当时它存在于用 PHP 编写的片假名判断函数中。在笔者负责的那个应用里,这个 Bug 把“宴”误当作片假名了。8
图 6-5 “ラリルレロ”中的第二、三字节和“宴”匹配成功
如果想再现这个问题的话,可以使用下面的脚本(注意文件需要以 Shift_JIS 编码方式保存)。程序的执行结果为 1(
strpos
的返回值是从源字符串的位置 0 开始计算的)。<?php $p = strpos(' ラリルレロ ', ' 宴 '); var_dump($p);
解决这个问题的方法其实很简单,那就是使用对应多字节字符串操作的
mb_strpost
来代替strpos
方法。此外,需要将内部字符编码方式 9 设为 Shift_JIS。下面我们再看一下将字符“表”的第二字节和日元符号“¥”匹配的例子,如图 6-6 所示。这也是引起系统漏洞的可能原因。
图 6-6 表的第二字节和 ¥ 匹配成功
上面两个例子都可以通过使用多字节版本的字符串处理函数来解决。另外,如果使用 UTF-8 (后面会涉及)的话,由于其本身从编码规则上来说不会导致类似上面问题的发生,可以说是一种比较安全的方法。
- 非法的 Shift_JIS 编码数据
现在已经有一些公开的方法可以通过使用非法的 Shift_JIS 数据进行攻击。非法的 Shift_JIS 编码数据(字节数据)指的是如下的数据。
- 只有 Shift_JIS 的前置字节而没有后置字节(比如 0x81)
- 紧随 Shift_JIS 前置字节的后置字节不在正确的范围之内(比如 0x81 0x21)
- 由非法 Shift_JIS 编码数据引起的 XSS
非法的 Shift_JIS 编码数据有时候可能引起 XSS 漏洞。请参考下面的示例代码。假设源文件以 Shift_JIS 编码保存并运行。
代码清单 63/63-001.php
<?php session_start(); header('Content-Type: text/html; charset=Shift_JIS'); ?> <body> <form action=""> 姓名 :<input name=name value="<?php echo htmlspecialchars($_GET['name'], ENT_QUOTES); ?>"><br> 邮箱地址 : <input name=mail value="<?php echo htmlspecialchars($_GET['mail'], ENT_QUOTES); ?>"><br> <input type="submit"> </form> </body>
当不带表单查询字符串(Query String)的时候,页面显示如下图 6-7 所示。
图 6-7 63-001.php 的页面显示
下面,我们再通过下面的 URL 来运行刚才的例子。
http://example.jp/63/63-001.php?name=1%82&mail=onmouseover%3dalert(document.cookie)//
如图 6-8 所示,可以看到页面里本来有两个的输入框现在变成只有一个了。
图 6-8 63-001.php 页面显示被修改
如果我们再把鼠标移动到输入框上面去,则 JavaScript 代码会被执行,弹出如图 6-9 那样的对话框。
图 6-9 植入的 JavaScript 代码被执行
这时候我们再来看看页面的源代码(关键部分),如下所示。
<input name=name value="1 · ><BR> 邮箱地址 : <input name=mail value=" onmouseover=alert(document.cookie)//"><BR>
这里只显示了表单里和 input 属性值相关的内容,其中由程序生成的内容以网格线显示。
图 6-10 应用程序生产的属性值
其中 0x82 是 Shift_JIS 编码里两字节字符中的第一个字节,很多浏览器(包括 Internet Explorer 和 Firefox 等),都将 0x82 和后面的 " 作为一个字符看待。所以本来表示属性值结束位置的双引号 " 被作为 Shift_JIS 编码字符的第二字节使用,直到 input 元素的“
value=
”为止(上述 HTML 代码中的阴影部分),都被作为前一元素的 value 属性来处理了。图 6-11 0x82 和 " 被合起来当作一个字符来处理
由于前一属性的值的闭合双引号一直延续到了下一个属性的“value=”,所以通过参数 mail= 指定的“
onmouseover=alert(document.cookie)//
”被“挤到”了属性值的外面,从而被识别成了 HTML 元素的鼠标事件绑定了。我们会在后面说明如何从根本上解决这个问题的。这里我们先看一下如何通过指定
htmlspecialchars
函数的第三个参数来设置正确的字符编码方式,以消除 XSS 隐患。代码清单 63/63-002.php(部分代码)
姓名 :<input name="name" value="<?php echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'Shift_JIS' ); ?>"><br> 邮箱地址 : <input name="mail" value="<?php echo htmlspecialchars($_GET['mail'], ENT_QUOTES, 'Shift_JIS' ); ?>"><br>
修改后的代码执行结果可以参考下面的图 6-12。 在这个图中,我们可以看到 onmouseover 事件作为纯文本显示在了文本框中,而没有被解释为 JavaScript 脚本来执行。
图 6-12 消除 XSS 隐患后
- 对字符匹配的影响
- EUC-JP
EUC-JP 是为了在 Unix 上处理日语而设计的字符编码方式。对于 US-ASCII 字符集的字符,EUC-JP 直接使用其编码,对于 JIS X 0208 字符集规定的日语字符,则使用两个字节的 0xA1~0xFE 范围。
图 6-13 是 EUC-JP 的各个字节的分布示意图
图 6-13 EUC-JP 的各字节分布
从上图可以看出,由于 2 字节长字符的后置字节不会和 1 字节字符发生重合,所以不存在 Shift_JIS 中的“5C”问题。但是,EUC-JP 里 2 字节长字符的前置字节和后置字节范围是一样的,所以如果将日语字符串移位一个字节的话,就会发生字符串匹配问题。图 6-14 是在字符串“ラリルレロ”中匹配“蛍”的例子。
图 6-14 字符串“ラリルレロ”中匹配“蛍”成功
下面的代码(需要将文件保存为 EUC-JP 编码方式)展示了如何再现这个问题。脚本的运行结果会打印出来显示 3(
strpos
的返回结果是从源字符串的位置 0 开始计算的)。<?php $p = strpos(' ラリルレロ ', ' 螢 '); var_dump($p);
要想解决这个问题也很简单,和 Shift_JIS 一样,使用多字节版本的
mb_strpos
就可以了。同样,内部字符编码方式需要设置为 EUC-JP。- 非法的 EUC-JP 编码数据
什么算得上是非法的 EUC-JP 编码数据?其条件和前面讲到的“非法的 Shift_JIS 编码数据”中提到的内容是一样的。而且,和 Shift_JIS 一样,非法 EUC-JP 编码数据也会造成系统漏洞。
- 非法的 EUC-JP 编码数据
- ISO-2022-JP
8 中文编码也有类似的问题,可以参考后面 GB2312 部分的相关内容。——译者注
9 mb.internal_encoding
ISO-2022-JP 采用的是 7 比特的字符编码方式,采用转义序列 10 的方式来在 US-ASCII 和 JIS X 0208 之间进行交替编码的方法。有时候 ISO-2022-JP 也被称为“JIS 编码”。图 6-15 是转义序列的一个例子,其表示的是用 ISO-2022-JP 编码方式的日语字符串“ABC と漢字!”在内存的存储情况。
10 Escape Sequence。通过一定的组合来表示不能直接显示的字符的方法。狭义上来指以转义字符“0x1B”即 ESC 开始的字符串。——译者注
图 6-15 ISO-2022-JP 字符串编码示例
在上面的图里,以“ESC $ B”开头的为 JIS X 0208 编码的数据,以“ESC ( B”开头的数据则为 US-ASCII 编码的数据。由于 ISO-2022-JP 交替使用了两种不同的编码方式,所以并不适合在计算机内部进行处理和查询等操作。这种编码方式主要用于在通信网络中进行数据传输,比较典型的使用场景就是电子邮件的传输。
也许大家听说过“在网络上不要使用半角片假名”这种说法,其由来也和 ISO-2022-JP 编码有关,因为 ISO-2022-JP 编码中并不支持半角片假名(JIS X 0201)。
以上我们已经针对 Shift_JIS、EUC-JP、ISO-2022-JP 等基于 JIS 系列字符集的编码方式进行了相应的说明,下面开始我们再来看看 Unicode 编码的两种主要编码方式:UTF-16 和 UTF-8。
- UTF-16
Unicode 在最初设计的时候曾想使用 16 比特的长度来容纳世界上所有的字符,所以当时直接使用 16 比特码位(Code Point)的编码方式 USC-2,这也是当时使用最普及的 Unicode 编码方式。但是之后 Unicode 长度扩展到了 21 比特,随之出现的是 UTF-16 编码方式。这种编码方式在兼容 UCS-2 的同时,也支持 BMP 之外的字符。
UTF-16 通过使用代理对(Surrogate Pair)技术来实现支持 BMP 之外的字符。它通过在 16 比特的 Unicode 范围内预留两个 1024(2 的 10 次方)字符长度的区域(0xD800~0xDBFF 以及 0xDC00~0xDFFF),这两个区域组合的话则一共可以表示 2 的 20 次方(大约 100 万)个字符。
我们来看一下具体的实例,比如 BMP 以外的日语汉字“
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论