使用 XML: 安全编码实践 (1)
http://tech.ddvip.com 2008年08月21日 社区交流
内容摘要:BenoÎt 检查了自己的项目笔记,整理了一份常见 XML 技术陷阱列表。在项目中研究这些潜在的问题可以避免很多挫折。本系列文章共有四篇,这是第一篇,BenoÎt 分析了 XML 语言本身的风险。
过去七年来,作为一名顾问、培训人员和作者,我有幸从一个特殊的角度见证了 XML 的发展和成熟。
XML 最初出现的时候,组织和开发人员对这种新的“标记语言——不管它是什么”彬彬有礼地保持怀疑态度。后来,随着使用 XML 解决的问题越来越多,他们变得热情起来。现在,很多开发人员和组织很自然地将 XML 纳入他们的项目之中。
不幸的是,应用的增加带来了滥用,从这个意义上说,XML 也反映了其他技术的采用过程。任何新技术的第一批用户通常都满怀热情(如果要让同事和客户承认这项技术的价值就必须如此),但是他们可能也有怀疑,因此通常会花费时间来研究如何最好地实现这种新技术。
随着技术的成熟,不断地得到承认。当这项技术在越来越多的应用程序中使用时,出现的错误也越来越多。所幸的是,同时经验也积累起来了:针对普遍问题经过测试的解决方案以及常见的陷阱出现并经过整理。
为了撰写这四篇文章,我翻阅了自己的笔记查找反复出现的 XML 陷阱。我希望将其整理出来并给出解决的方法,帮助您避免成为这一技术常见问题的牺牲品。
首先从最基本的一层——XML 本身开始。坚持共同的语法是建立可靠应用程序的第一步。这一部分讨论三个常见的问题:
解析器和字符转义的使用
编码
名称空间
后续文章将探讨如何可靠地使用 XML 文档、如何验证和测试 XML 文档、如何建立 XML 和其他文件格式的接口,如图像、视频、字处理等等。
文雅的语法
第一节介绍 XML 语法的一些基本知识。如果对此已经非常精通,完全可以跳过这一节。
XML 语法很简单:最基本的一点就是开标签和闭标签必须匹配。但是我希望以后不会再受到这样的电子邮件,“我尝试用这样那样的工具处理附件中的 XML 文档,但是行不通,还有其他的工具吗?”毫无例外的,我打开文档后总能找到明显的语法错误,比如没有反斜杠的空标签(应该是这样: <empty/>)。如果文档不完全符合 XML 语法,就不是一个 XML 文档;如果不是 XML 文档,XML 工具就不能处理它。XML 有一种非常精确而正式的语法。一个文档要么完全符合语法,要么不被看作是 XML 文档。仅此而已。
相反,有些应用程序可能不认可无安全有效的文档。应用程序可能没有完全实现该语法,因而无法识别,比方说字符实体(如 î)。
XML 的问题时看起来太简单。编写某个东西常常看起来比学习另一个组件更容易、更快捷。在封闭的环境中,应用程序读取自己生成的文档,这种方法可能奏效,但是在多个应用程序使用文档的产品环境不大可能。
解决之道
所幸的是,使用 XML 解析器很容易避免这种问题。每种编程语言都由适用的 XML 解析器(即使 Cobol 都有强大的 XML 支持),没有理由不使用。
作为开发人员,您有两种选择:XML 解析器或者编组组件。如果需要底层控制 XML 文档的解码,应该使用 XML 解析器。对于本文而言,解析器使用 DOM、JDOM、SAX 或 StAX 都没有关系,但真正的 XML 解析器是正确读取 XML 文档的唯一保证。
如果不需要更多地控制解析过程,可以找到更方便的编组组件,如 JAXB、Castor 或 Axis。编组组件直接在 XML 标签和 Java™ 对象之间映射。 JAXB 和 Castor 是为了处理文件中的文档设计的,Axis 用于 Web 服务。编组组件内嵌有 XML 解析器,可以确信它们完全实现了语法。
虽然建议使用解析器读取 XML 文档,如果自己实现了写文档例程,也可以避开解析器。读取 XML 文档是一项复杂的任务,因为读者必须支持完整的语法,但是写 XML 文档相对容易一些,因为可以避开语法的一个子集:如果不需要属性,就不需要支持属性;如果不需要多种编码,就不需要支持多种编码,依次类推。
这里唯一的陷阱是需要正确地转义保留字符(参见表 1)。特别要注意实体字符(如 î),因为它们依赖于文档编码(请参阅“编码的问题”)。
表1. 保留
| 字符 | 转义序列 | 说明 |
| < | < | |
| & | & | |
| > | > | |
| ' | ' | 仅用于属性,如果使用 " 作为分隔符 |
| " | " | 仅用于属性,如果使用 ' 作为分隔符 |
| 其他 | &#unicode; | 当前编码中不支持的任何字符 |
类似 清单 1 所示的一个简单循环通常就足够了。更有效的实现该功能是可能的,但如果写入 UTF-8 或 UTF-16 流,清单 1 在语法上是有效的(否则,还需要将某些字符转义成字符实体)。
清单 1. 繁琐的转义实现
// assumes UTF-8 or UTF-16 as encoding,
public String escape(String content)
{
StringBuffer buffer = new StringBuffer();
for(int i = 0;i < content.length();i++)
{
char c = content.charAt(i);
if(c == '<')
buffer.append("&lt;");
else if(c == '>')
buffer.append("&gt;");
else if(c == '&')
buffer.append("&amp;");
else if(c == '"')
buffer.append("&quot;");
else if(c == ''')
buffer.append("&apos;");
else
buffer.append(c);
}
return buffer.toString();
}
有些开发人员更喜欢 CDATA 节而非转义。CDATA 是表明某一部分文档可能包含非转义保留字符的一种机制。比如,<condition><![CDATA[a > 4]]></condition>。本系列的第三篇文章中,我还将讨论 CDATA 节,现在只要知道它们比转义更安全就足够了,因为 CDATA 节不能包含另一个 CDATA 节。
更灵活的解决之道是求助于转换程序,请参阅我的技巧文章“Implement XMLReader”(developerWorks)。
其他方法
如果必须和违反 XML 语法的应用程序打交道,又无法说服开发人员修改他/她的应用程序,怎么办呢?
我发现,更好的办法是认为这类应用程序根本不生成 XML,增加一个步骤将这种不正常的 XML 转化成正确的 XML。为什么要增加一个步骤呢?因为这样可以隔离不符合标准的地方,能够使用任何 XML 工具进行后续的处理。
编码的问题
编码的使用可能带来更严重的问题。开发人员常常忽视了一个问题,编码没有限制 XML 支持的字符集。每个 XML 文档都支持完整的 Unicode 字符集(XML 1.1 中是 16-bit 或 32-bit 字符)。
对 XML 文档编码可以缩小文档的大小,但是并没有限制文档使用特定的 Unicode 子集,这要感谢字符实体。事实上,通过字符实体,可以插入 Unicode 编码表中的任何字符,即使文档是用最严格的编码(US-ASCII,只适合四种语言:English、Hawaiian、Latin 和 Swahili)。
这是一个问题,因为 Java 应用程序或者最新版本的 DB2® 可能支持 Unicode,但是很少有遗留系统也支持 Unicode。因此如果 XML 流进入遗留应用程序,可能需要解决 Unicode 的问题。为了避免误解,我们再说明一次,强制采用某种编码不能解决问题,因为如上所述,可以将特殊字符转义成字符实体。
因为很少有可能重写遗留系统,就需要某种转换例程将 Unicode 字符转化成应用程序能够接受的字符集,比如将“î”转化成直接的“i”(去掉抑扬符号)。多数 XML 解析器都提供了操纵 Unicode 字符的例程。
名称空间问题
本文中第三个也是最后一个问题的来源是 XML 名称空间。
引入名称空间是为了管理 XML 词汇表,避免标签同名。不同上下文中的两个词汇表常常使用同一个标签。比如,消息词汇表中可能包含 subject、date、from、to 和d body 这类标签(参见 清单 2),而数字资产词汇表可能包含 subject、date、description、camera 和 frame number 这类标签(参见清单 3)。
清单 2. 消息词汇表
<envelope>
<subject>Test memo</subject>
<date>April 26, 2005</date>
<from>jack@writeit.com</from>
<to>john@xmli.com</to>
<body>memo body goes here</body>
</envelope>
清单 3. 数字资产词汇表
<photo>
<subject>Westlicht Museum of Camera and Photography, Vienna</subject>
<date>April 25, 2005</date>
<description>Lobby of the museum</description>
<camera>Nikon D70</camera>
<frame>5643</frame>
</photo>
数字资产如果通过消息平台发送就会出现冲突,因为软件混淆了两个词汇表中的 subject 和 date 标签。换句话说,标签名不是全局标识符。
XML 名称空间通过在标签名前增加全局标识符将本地名转化成全局名。为了保证全局标识符的唯一性,全局标识符必须是 URI(就是说很可能包含注册的域名以保证唯一性)。结果如清单 4 所示。
清单 4. 组合词汇表
<env:envelope xmlns:env="http://psol.com/2005/env"
xmlns:ph="http://psol.com/2005/photo">
<env:subject>Latest photo</env:subject>
<env:date>April 27, 2005</env:date>
<env:from>jack@writeit.com</env:from>
<env:to>john@xmli.com</env:to>
<env:body>
<ph:photo>
<ph:subject>Westlicht Museum
of Camera and Photography, Vienna</ph:subject>
<ph:date>April 25, 2005</ph:date>
<ph:description>Lobby of the museum</ph:description>
<ph:camera>Nikon D70</ph:camera>
<ph:frame>5643</ph:frame>
</ph:photo></env:body>
</env:envelope>
我来澄清常常被误解的两件事:
URI 是标识符,不是地址。
前缀不是标识符。
URI 和地址
虽然一般的 URI 是地址,但对于 XML 名称空间来说,它们仅作为标识符来使用。。我希望名称空间像 Java 包那样来标识,但是不能这样做——比如用 com.psol.vocabulary 代替更容易造成混淆的 http://psol.com/vocabulary。
既然是标识符,这个地址就可能是无效的,就是说如果尝试打开该地址就会返回“404 - Resource not found”错误。但它们仍然用于这个目的。和一般的想法相反,名称空间 URI 并不指向 W3C XML Schema。
其次,因为这个上下文中 URI 是标识符,应用程序必须逐字符地匹配该 URI。修改 XML 词汇表的 URI,比方说让它指向您的服务器,这种做法是错误的。比如,XSL 的 URI 是 http://www.w3.org/1999/XSL/Transform。如果您在BM®工作,也不能将其改为 http://www.ibm.com/1999/XSL/Transform。事实上,根本不能改变已有词汇表的 URI。
我在讲授 XSLT 时,学生常常抱怨处理程序不工作,而实际上是他们没有正确的复制 XSLT URI。
结论之一是,应该避免修改名称空间。在 URI 中包含版本模式通常不是一个好主意,这样注定会让应用程序无法工作(不错,我承认 W3C 对 SOAP 是这样做的)。
前缀
另一种常见的错误是混淆了前缀和标识符。标签名不是标识符,同样,前缀也不是标识符。两个不同应用程序使用同一前缀的风险很大。因此,名称空间前缀是透明的,不应该在应用程序中显式地操纵前缀。但是,XML 作者在文档中修改前缀是完全合理的(比如为了避免冲突)。
因此应该避免清单 5 这样的代码, 而仿效清单 6 中的写法。
清单 5. 不正确的前缀测试
startElement(Stringuri,Stringlocal,Stringqname,Attributesatts)
{
if(qname.equals("env:Envelope"))
; // do something
}
清单 6. 正确的测试名称空间 URI
startElement(Stringuri,Stringlocal,Stringqname,Attributesatts)
{
if(uri.equals("http://psol.com/2005/envelope")
&& local.equals("Envelope"))
; // do something
}
结束语
只要记住这些陷阱,就可以在很大程度上改进您的 XML 编码。更重要的是,这样可以降低不兼容的危险,极大地简化 XML 应用程序的维护。本系列的后续文章将考察和 XML 应用而非语法有关的常见陷阱。
来源:developerWorks 作者:BenoÎt Marchal 责编:豆豆技术应用