为什么python decode会替换编码字符串中的无效字节?

时间:2010-03-30 17:33:47

标签: python security unicode screen-scraping

尝试解码无效的编码utf-8 html页面会产生不同的结果 python,firefox和chrome。

测试页面中的无效编码片段看起来像'PREFIX\xe3\xabSUFFIX'

>>> fragment = 'PREFIX\xe3\xabSUFFIX'
>>> fragment.decode('utf-8', 'strict')
...
UnicodeDecodeError: 'utf8' codec can't decode bytes in position 6-8: invalid data

更新:此问题在bug report到Python unicode组件中得出结论。据报道,该问题已在Python 2.7.11和3.5.2中修复。


以下是用于处理解码错误的替换策略 Python,Firefox和Chrome。注意它们是如何不同的,特别是如何 python builtin删除有效的S(加上无效的字节序列)。

的Python

内置replace错误处理程序替换了无效的\xe3\xab加上 S {+ 1}}来自U + FFFD

SUFFIX

浏览器

要测试浏览器如何解码无效的字节序列,将使用cgi脚本:

>>> fragment.decode('utf-8', 'replace')
u'PREFIX\ufffdUFFIX'
>>> print _
PREFIX�UFFIX

提供的Firefox和Chrome浏览器:

#!/usr/bin/env python
print """\
Content-Type: text/plain; charset=utf-8

PREFIX\xe3\xabSUFFIX"""

为什么PREFIX�SUFFIX 的内置replace错误处理程序正在从str.decode

中删除S

(更新1)

根据维基百科UTF-8(感谢mjv), 以下字节范围用于指示序列的开始 字节

  • 0xC2-0xDF:2字节序列的开始
  • 0xE0-0xEF:3字节序列的开始
  • 0xF0-0xF4:4字节序列的开始

SUFFIX测试片段有 0xE3 ,它指示python解码器 随后是一个3字节的序列,序列被发现无效并且是python 解码器忽略整个序列,包括'PREFIX\xe3\abSUFFIX',然后继续 忽略从中间开始的任何可能的正确序列。

这意味着对于像'\xabS'这样的无效编码序列,它会 解码'\xF0SUFFIX'而不是u'\ufffdFIX'

示例1:引入DOM解析错误

u'\ufffdSUFFIX'

示例2:安全问题(另请参阅Unicode security considerations):

>>> '<div>\xf0<div>Price: $20</div>...</div>'.decode('utf-8', 'replace')
u'<div>\ufffdv>Price: $20</div>...</div>'
>>> print _
<div>�v>Price: $20</div>...</div>

示例3:删除抓取应用程序的有效信息

>>> '\xf0<!-- <script>alert("hi!");</script> -->'.decode('utf-8', 'replace')
u'\ufffd- <script>alert("hi!");</script> -->'
>>> print _
�- <script>alert("hi!");</script> -->

使用cgi脚本在浏览器中呈现:

>>> '\xf0' + u'it\u2019s'.encode('utf-8') # "it’s"
'\xf0it\xe2\x80\x99s'
>>> _.decode('utf-8', 'replace')
u'\ufffd\ufffd\ufffds'
>>> print _
���s

渲染:

#!/usr/bin/env python
print """\
Content-Type: text/plain; charset=utf-8

\xf0it\xe2\x80\x99s"""

是否有任何官方推荐的处理解码替换的方法?

(更新2)

public review中,Unicode技术委员会选择了选项2 以下候选人:

  1. 用一个U + FFFD替换整个格式错误的子序列。
  2. 用一个U + FFFD替换格式错误的子序列的每个最大子部分。
  3. 用一个U + FFFD替换格式错误的子序列的每个代码单元。
  4. UTC分辨率为2008-08-29,来源:http://www.unicode.org/review/resolved-pri-100.html

    UTC Public Review 121还包括一个无效的字节流作为示例 �it’s ,它显示每个的解码结果 选项。

    '\x61\xF1\x80\x80\xE1\x80\xC2\x62'

    在普通Python中,三个结果是:

    1. 61 F1 80 80 E1 80 C2 62 1 U+0061 U+FFFD U+0062 2 U+0061 U+FFFD U+FFFD U+FFFD U+0062 3 U+0061 U+FFFD U+FFFD U+FFFD U+FFFD U+FFFD U+FFFD U+0062 显示为u'a\ufffdb'
    2. a�b显示为u'a\ufffd\ufffd\ufffdb'
    3. a���b显示为u'a\ufffd\ufffd\ufffd\ufffd\ufffd\ufffdb'
    4. 以下是python对无效示例字节流的作用:

      a������b

      再次,使用cgi脚本来测试浏览器如何呈现错误编码的字节:

      >>> '\x61\xF1\x80\x80\xE1\x80\xC2\x62'.decode('utf-8', 'replace')
      u'a\ufffd\ufffd\ufffd'
      >>> print _
      a���
      

      Chrome和Firefox都呈现:

      #!/usr/bin/env python
      print """\
      Content-Type: text/plain; charset=utf-8
      
      \x61\xF1\x80\x80\xE1\x80\xC2\x62"""
      

      请注意,浏览器呈现的结果与PR121 recomendation的选项2匹配

      虽然选项3 在python中很容易实现,但选项2和1 是一个挑战。

      a���b
      

4 个答案:

答案 0 :(得分:10)

你知道你的S是有效的,有前瞻和后见之明的好处:-)假设那里最初有一个合法的3字节UTF-8序列,第3个字节在传输中被破坏了......随着你提到的改变,你会抱怨一个虚假的S没有被替换。没有“正确”的方法,没有纠错码,水晶球或tamborine的好处。

更新

正如@mjv所说,UTC问题是应该包括多少 U + FFFD。

事实上,Python没有使用UTC的3个选项中的任何一个。

这是UTC唯一的例子:

      61      F1      80      80      E1      80      C2      62
1   U+0061  U+FFFD                                          U+0062
2   U+0061  U+FFFD                  U+FFFD          U+FFFD  U+0062
3   U+0061  U+FFFD  U+FFFD  U+FFFD  U+FFFD  U+FFFD  U+FFFD  U+0062

这是Python的作用:

>>> bad = '\x61\xf1\x80\x80\xe1\x80\xc2\x62cdef'
>>> bad.decode('utf8', 'replace')
u'a\ufffd\ufffd\ufffdcdef'
>>>

为什么?

F1应该启动一个4字节序列,但E1无效。一个坏的序列,一个替换。
在下一个字节再次开始,第三个80. Bang,另一个FFFD 再次从C2开始,它引入了一个2字节的序列,但C2 62无效,所以再次爆炸。

有趣的是,UTC没有提到Python正在做什么(在引导字符指示的字节数之后重新启动)。也许这在Unicode标准的某个地方实际上是被禁止或弃用的。需要更多阅读。看这个空间。

更新2 休斯顿,我们遇到了问题

===引自Chapter 3 of Unicode 5.2 ===

转化过程的限制

不要将字符串中任何格式错误的代码单元子序列解释为字符的要求(参见符合性条款C10)对转换过程有重要影响。

例如,这些过程可以将UTF-8代码单元序列解释为Unicode字符 序列。如果转换器遇到格式错误的UTF-8代码单元序列 以有效的第一个字节开头,但不会继续有效的后继字节(参见 表3-7),它不能使用后继字节作为格式错误的子序列的一部分 每当这些后继字节本身构成格式良好的UTF-8代码的一部分 单位子序列

如果在遇到第一个错误时UTF-8转换过程的实现停止, 没有报告任何格式错误的UTF-8代码单元子序列的结束,那么 要求几乎没有实际意义。但是,要求确实引入了 如果UTF-8转换器继续超过检测到的错误点,则显着限制 也许通过将一个或多个U + FFFD替换字符替换为无法解释, 格式错误的UTF-8代码单元子序列。例如,输入UTF-8代码 单元序列<C2 41 42>,这样的UTF-8转换过程不得返回 <U+FFFD><U+FFFD, U+0042>,因为这些输出中的任何一个都是错误解释格式良好的子序列作为不良后续子序列的一部分的结果。该 此类流程的预期回报值为<U+FFFD, U+0041, U+0042>

对于使用有效后继字节的UTF-8转换过程,不仅不符合, 但也让转换器对安全漏洞开放。请参阅Unicode技术报告 #36,“Unicode安全注意事项。”

===报价结束===

然后继续详细讨论“发射多少FFFD”问题。

在最后的第二段中使用他们的例子:

>>> bad2 = "\xc2\x41\x42"
>>> bad2.decode('utf8', 'replace')
u'\ufffdB'
# FAIL

请注意,这是str.decode('utf_8')的'replace' 'ignore'选项的问题 - 这都是关于省略数据,而不是关于发射多少U + FFFD;让数据发射部分正确,U + FFFD问题自然消失,正如我没有引用的那部分所解释的那样。

更新3 当前版本的Python(包括2.7)将unicodedata.unidata_version设置为'5.1.0',这可能会也可能不会表明与Unicode相关的代码旨在符合Unicode 5.1。 0。无论如何,在5.2.0之前,Unicode标准中没有出现对Python正在做什么的冗长禁止。我将在Python跟踪器上提出一个问题而不提及'oht'.encode('rot13')这个词。

举报 here

答案 1 :(得分:8)

0xE3字节是指示3字节字符的一个(可能的)第一个字节。

显然,Python的解码逻辑占用了这三个字节,并尝试解码它们。它们与实际代码点(“字符”)不匹配,这就是Python生成UnicodeDecodeError并发出替换字符的原因
然而,似乎在这样做时,Python的解码逻辑不遵守Unicode Consortium 关于“格式错误”的UTF-8序列的替换字符的建议。 / p>

有关UTF-8编码的背景信息,请参阅维基百科上的UTF-8 article

新(最终?)修改:重新UniCode Consortium's recommended practice for replacement characters(PR121)
(顺便说一下,恭喜 dangra 继续挖掘和挖掘,从而使问题更好)
对于这个建议的解释, dangra 和我都以我们自己的方式部分不正确;我最近的见解是,这个建议确实也说明了尝试和“重新同步” 关键概念是 最大子部分 [形成错误的序列]
鉴于PR121文档中提供的(单个)示例,“最大子部分”意味着 读入字节,这些字节不可能是序列的一部分。例如,序列中的第5个字节,0xE1不可能是“序列的第二个,第三个或第四个字节”,因为它不在x80-xBF范围内,因此这会终止开始的错误序列用xF1。然后必须尝试用xE1等开始一个新的序列。同样,在击中x62时也不能解释为第二个/第三个/第四个字节,坏序列结束,“b”(x62)是“保存的” ...

从这个角度来看(直到纠正;-))Python解码逻辑似乎有问题。

另请参阅 John Machin 在本文中的回答,了解基础Unicode标准/建议的更具体的引用。

答案 2 :(得分:5)

'PREFIX\xe3\xabSUFFIX'中,\xe3表示它和接下来的两个咬合形成一个unicode代码点。 (\xEy适用于所有y。)但是,\xe3\xabS显然不会引用有效的代码点。由于Python知道假设占用三个字节,因此它无论如何都会吸收所有三个字节,因为它不知道你的S是S而不仅仅是因为某些其他原因而代表0x53的字节。

答案 3 :(得分:0)

  

此外,是否有任何unicode官方推荐的处理解码替换的方法?

没有。 Unicode认为它们是错误条件,不考虑任何回退选项。所以上面的行为都没有“正确”。