acm-header
登录

ACM通信

实践

重新考虑稳健性原则


做什么要保守,接受什么要自由

信贷:亚当·海耶斯

回到顶部

“做事情要保守,接受别人的东西要自由。”(RFC 793)

1981年,Jon Postel制定了鲁棒性原则,也称为Postel定律,作为当时新TCP的基本实现指南。鲁棒性原则的目的是最大化网络服务实现之间的互操作性,特别是在面对模糊或不完整的规范时。如果生成某个协议片段的某个服务的每个实现都使用规范的最保守解释来实现,而接受该协议片段的每个实现都使用最慷慨的解释来解释它,那么两个服务能够彼此交谈的机会就会最大化。阿帕网的经验表明,让独立开发的实现实现互操作是困难的,而且由于Internet的规模预期要比阿帕网大得多,旧的临时方法需要得到增强。

尽管健壮性原则是专门为TCP的实现描述的,但它很快就被人们接受为实现一般网络协议的一个很好的主张。有些人将其应用到api的设计甚至编程语言的设计中。它很简单,容易理解,直观易懂。但这是正确的吗?

多年来,稳健性原则一直是公认的教条,但当它被忽视而不是被实践时,更容易失败。然而,近年来,这一原则受到了挑战。这并不是因为实现者变得更加愚蠢,而是因为世界变得更加充满敌意。健壮性原则影响了两个普遍的问题领域:有序的互操作性和安全性。

回到顶部

标准和互操作性

网络协议实现中的互操作性是一个难题。这有很多原因,都归结为一个基本的事实:电脑是不可原谅的。例如,规范可能是模糊的:两个工程师构建满足规范的实现,但这些实现仍然不能相互通信。该规范可能实际上是明确的,但措词的方式有些人误解了它。可以说,一些最重要的规格属于这一类,因为它们是用法律术语编写的,这对大多数工程师来说是不自然的。规范可能没有考虑到某些情况(例如,硬件故障),这可能导致在现实世界中实现工作需要违反规范的情况。

类似地,规范可能会对环境做出隐含的假设(例如,硬件支持的网络数据包的最大大小或相关协议的工作方式),而这些假设是不正确的或环境发生了变化。最后,通常情况下,一些实现者可能会发现需要增强协议,以添加规范中没有定义的新功能。

编写标准(即定义不同实现之间互操作性的任何规范)是一门艺术。在法律意义上,标准本质上是契约,但法律的优势(或劣势)在于长期以来定义、重新定义和细化定义,通常在判例法中。标准的目标是使互操作性成为可能。这既需要精确(以避免歧义)又需要清晰(以避免误解)。任何一种方式的失败都将导致缺乏互操作性。不幸的是,如前所述,这两个目标有时是不一致的。

我们正常的人类语言常常是含糊不清的;在现实生活中,我们毫无困难地处理这些歧义(或将它们用作笑话的基础),但在技术世界中,它们可能会引起问题。然而,极其精确的语言对我们来说是如此不自然,以至于很难领会其中的微妙之处。标准通常使用形式语法、数学方程和有限状态机,以便简洁地传递精确的信息,这当然有帮助,但这些通常不能独立存在,例如,语法描述的是语法而不是语义,方程必须翻译成代码,而有限状态机是出了名的难以被人类理解。

标准通常包括图表和例子来帮助理解,但这些实际上会产生问题。考虑图表与描述性文本不匹配的可能性。哪一个是正确的?就这一点而言,任何时候,同一件事在两个地方被描述时,这两个描述可能会说微妙的不同的东西,这是危险的。例如,RFC 821和RFC 822都描述了电子邮件地址的语法,但不幸的是,它们在一些微小的方面有所不同(这些标准已经更新,以解决这个和其他问题)。一种常见的解决方案总是“通过引用”包含必要的重复语言(也就是说,包含对另一个文档的引用而不是实际描述)。当然,在极端情况下,这可能导致标准文档堆积如山。例如,消息处理(电子邮件)的OSI建议(标准)包含在大约20个不同的文档中,其中充满了交叉引用。


多年来,稳健性原则一直是公认的教条,但当它被忽视而不是被实践时,更容易失败。然而,近年来,这一原则受到了挑战。


即使使用例子也会引起争议。例子是从来没有规范(标准术语权威);也就是说,如果示例和文本主体之间存在冲突,则文本获胜。此外,例子很少是完整的。他们可能会演示协议的某些部分,但不会演示所有细节。理论上,如果你从一个标准中去掉所有的例子,那么这个标准的意义就不会改变存在的理由帮助理解。问题是,一些实现者阅读示例(这些示例通常比标准的实际文本更容易理解)并从中实现,因此遗漏了标准的重要细节。这导致一些标准的作者完全避免使用示例。

一些(通常是供应商驱动的)标准使用“参考实现”方法,也就是说,一个被定义为正确的实现;当且仅当它们与参考实现相反时,所有其他实现都是正确的。这种方法充满了危险。首先,没有一个实现是完全没有bug的,所以在参考实现中找到并修复bug本质上改变了标准。

类似地,标准通常有各种“未定义的”或“保留的”元素,例如,同时指定多个语义重叠的选项。其他实现将发现这些未定义的元素是如何工作的,然后依赖于那些非预期的行为。当扩展引用实现以添加功能时,这会产生问题;这些未定义和保留的元素通常用于提供新函数。此外,可能存在两个独立的实现,每个实现都针对参考实现工作,但彼此之间没有冲突。综上所述,参考实现方法与书面规范相结合可能是有用的,特别是当该规范正在被改进时。

最初的InterOp会议旨在允许具有网络文件系统(NFS)实现的供应商测试互操作性,并最终公开演示它们可以互操作。最初的11天仅限于少数工程师,所以他们可以聚在一个房间里,让他们的东西一起工作。当他们走进房间时,供应商大多只针对他们自己的系统,也可能针对Sun的系统(因为作为NFS的最初开发人员,Sun当时拥有参考实现)。漫漫长夜都被用来争论规范中的歧义。在那11天结束的时候,大门向客户敞开了,这时大多数(但不是所有)系统都与其他系统相对抗。到那次会议结束时,NFS协议得到了更好的理解,许多错误得到了修复,标准也得到了改进。这是实现驱动标准的必经之路。

制定标准的另一种方法是让一群聪明人在一个房间里集思广益,讨论标准应该做什么,只有在标准编写完成之后,代码才应该实现。这与传统的软件工程最接近,即在代码之前编写规范。举个极端的例子,这就是瀑布模型。以这种方式产生标准的问题与瀑布模型发生的问题是一样的:规范(标准)有时强制要求的东西只有少量的用处,但很难或不可能实现,并且返回和修改规范的成本随时间呈指数上升。

也许最好的情况是标准和实现是并行开发的。当SMTP(简单邮件传输协议)正在开发时,我处于一个不同寻常的位置,即在开发标准的同时开发Sendmail软件。当对标准草案的更新被提出时,我能够立即实现它们,通常是在一夜之间,这使得标准和实现能够一起发展。标准中的歧义很快就暴露出来了,好心的功能实现起来不必要地困难。不幸的是,这种情况在今天是罕见的,至少部分原因是世界已经变得足够复杂,这样快速更新标准不再容易。

回到顶部

标准中的歧义和可扩展性

作为一个模棱两可的例子,考虑以下摘自(虚构的)标准:

如果一个选项在数据包中指定X包含参数的值。

这假设协议具有固定大小的报头。一个可能是在旗子区,然后呢X是数据包中的某个字段。从表面上看,这一描述似乎很清楚,但它并没有具体说明是什么领域X意味着,如果一个选择是指定。更好的表达方式可能是:

如果一个选项在数据包中指定X包含参数的值;否则领域X必须是零。

你可能会认为这种措辞应该是不必要的当然X应该是零,为什么要这么明确呢?但如果没有这个细节,它也可能意味着:“如果一个选项没有在数据包中指定X是被忽略的“或者,也许,”字段X是未定义的。”这两者都与“必须为零”的解释有本质区别。此外,这两个词之间的差异虽然微不足道,但意义重大。在前一种情况下,“ignored”可能意味着“必须被忽略”(也就是说,在任何情况下都不应该字段X使用if选项一个没有指定)。但后一种情况允许场的可能性X可能会被用于其他目的。

这(最终)将我们带回了鲁棒性原则。给定“必须为零”规范,为了最健壮,任何实现都必须为零X字段在发送数据包之前(在它发送的内容中要保守),但不会检查X收到后立即投递(对所接受的内容要宽容)。

现在假设我们的标准修改了(版本2),增加了aB选项(不能与选项一起使用一个),它也使用X字段。健壮性原则拯救了我们:因为“健壮的”版本1实现不应该检查字段的值X除非选择一个已经指定了,添加一个选项会不会有问题B.当然,版本1的接收器不能提供这个选项B功能,但是当他们收到版本2的数据包时,他们也不会感到沮丧。这是一件好事:它允许我们在不破坏旧实现的情况下扩展协议。

这也阐明了在传递packetimplemements should时应该做什么明确的领域X尽管这是最“保守”的做法,因为这将打破版本1实现在两个版本2实现之间转发数据包的情况。在这种情况下,健壮性原则必须包含一个推论:实现应该静默地忽略并传递它们不理解的任何内容。换句话说,“保守”有两种直接冲突的定义。

现在让我们假设我们的神话标准有另一个领域Y它是为将来使用准备的,也就是说,在协议扩展中。描述此类字段的方法有很多,但常见的例子是将它们标记为“保留”或“必须为零”。前者并没有说明兼容的实现应该使用什么值来初始化保留字段,而后者做了,但通常假设0是一个很好的初始化式。应用鲁棒性原则可以很容易地看到,当协议的版本3使用字段发布时Y这没有问题,因为所有旧的实现都将在该字段中发送零。

回到顶部

黑暗的一面

但如果有些实现没有设置字段会发生什么Y为零?这个字段从来没有初始化过,因此它保留了以前在内存中发生的任何垃圾。在这种情况下,不正确(不够保守)的实现可能会愉快地存活下来,并与其他实现进行互操作,即使它在技术上与规范不匹配。(这样的实现也可能会泄露信息,这是一个安全问题。)或者,实现可以征用Y字段用于其他用途(“毕竟,它没有被使用,所以我不妨使用它”)。效果是一样的。

这里所发生的是糟糕的实现幸存下来了,因为所有其他实现都是慷慨的。除非协议版本3出现,或者某些实现违反了鲁棒性原则的“接受”方面,并开始进行更仔细的检查,否则永远不会检测到这一点。事实上,一些协议实现具有特殊的测试模块,这些模块“在接受的内容方面比较保守”,以便找出这些问题,但许多协议实现并没有这样做。


有时互操作性和安全性是相互矛盾的。在今天的环境下,它们都是必不可少的。必须取得某种平衡。


最后一种侮辱可能发生在开发版本3时,我们发现太多的实现(或一个非常流行的实现)在生成的内容中不够保守。为了避免您认为这种情况一定很罕见,有无数的情况是,供应商找到了一个方便的“保留”字段并将其征用为自己使用。

我把这个例子框定成好像它涉及到低级网络数据包(à la在传输控制协议RFC 793中的图3),2但这可以很容易地应用于XML等通用协议。同样的困难(征用稍后使用的标记,删除无法识别的属性,等等)也适用于这里。(关于为什么健壮性原则不应该应用于XML的一个很好的论证,请参见Tim Bray的《再次讨论Postel》。1

回到顶部

安全

鲁棒性原则是在合作者的互联网中制定的。从那以后,世界发生了很大的变化。一切都是可疑的,甚至是那些你自认为可以控制的服务。需要检查的不仅仅是用户输入,数据处理器还可能包括DNS(域名系统)结果、数据库查询结果、HTTP应答码等任意数据。每个人都知道检查缓冲区溢出,但检查传入数据远远不止于此。

  • 您可能会倾向于相信自己的公司数据库,但请考虑一下数据是如何添加的。你相信每一个可能更新数据库的软件都能做严格的检查吗?如果没有,你应该自己做检查。
  • 您是通过TCP连接通过防火墙获取数据的吗?你考虑过连接被劫持的可能性吗?安全关键数据应该只在加密的、签名的连接上被接受。其他数据应仔细核查。
  • 您信任来自防火墙内计算机的连接吗?你听说过病毒吗?甚至你认为在你控制下的机器也可能被破坏了。
  • 您信任命令行标志和环境变量吗?如果有人成功地在您的系统上获得了一个帐户,这些帐户可能用于权限升级。

互联网的氛围已经发生了如此大的变化,健壮性原则必须被严格地重新解释。对你所接受的东西过于宽容会导致安全问题。有时互操作性和安全性是相互矛盾的。在今天的环境下,它们都是必不可少的。必须取得某种平衡。

回到顶部

过分的慷慨

稳健性原则两边都可能出现错误。前面的“归零”X“Field”的例子是一个在生成内容上过于保守的例子,但大部分的重新评价来自于“在接受内容上要自由”的方面。

问题在于要走多远。不验证“必须为零”的字段实际上是零可能是合理的,您不必解释这些字段,也就是说,您可以将它们视为未定义的。作为一个真实的例子,SMTP规范规定实现必须允许行的长度不超过998个字符,但许多实现允许任意长度;接受较长的行可能是可以的(事实上,较长的行经常发生,因为许多软件将段落传输为单行)。类似地,尽管SMTP被定义为7位协议,但许多实现处理8位字符没有问题,而且世界上很多地方都依赖于此(现在有一个SMTP扩展来指定形式化此行为的8位字符)。

另一方面,必须处理数字签名的实现可能应该非常严格地解释公共证书。它们通常被编码为BASE64,使用65个字符进行编码(大小写字符、数字、“+”、“/”和“=”,所有这些都可以用7位US-ASCII表示)。如果证书中出现其他字符,则可能是安全攻击的结果,因此可能应该拒绝。在这种情况下,过于自由的实现可能会忽略无法识别的字符。

事实上,软件可靠性和安全性的原则之一就是始终检查您的输入。有些人将此解释为用户输入,但在许多情况下,这意味着检查所有内容,包括来自本地“协作”服务的结果,甚至函数参数。这个合理的原则可以总结为“在你接受的东西上保持保守”。

回到顶部

普遍性

我已经描述了这个问题,就像我在研究一个协议,如TCP,在包中具有固定位字段。事实上,它比这要普遍得多。考虑一些现实世界的例子:

  • 一些MIME实现解释MIME报头字段,如Content-Type或Content-Transfer-Encoding,即使没有包含MIME- version报头字段。这些实现中的不一致是收到错误消息的原因之一。
  • 太多的Web服务器接受来自用户和其他服务的任意数据,并在没有首先检查数据的合理性的情况下处理它们。这就是SQL注入攻击的原因。注意,这也可能走向另一个极端:许多Web服务器不允许您指定带有“+”的电子邮件地址,尽管这是完全合法的(而且有用)。这就是你在接受方面过于保守的一个明显例子。
  • 当指令中包含的位模式被认为是非法的(或者至少是未定义的)时,老式的小型计算机通常会做一些有用的事情。硬件不会抛出错误(代价很高),而是做任何对硬件设计人员方便的事情。过于复杂的软件工程师有时会发现这些奇怪的东西并使用它们,这导致在升级硬件时程序无法工作。DEC PDP-8就是这种机器的一个例子。事实证明,让机器在一条指令中向左或向右移动是可能的。在一些模型上,这有清除高阶位和低阶位的效果,但在其他模型上,它有其他作用。
  • Sendmail一直被批评在接受方面过于自由。例如,Sendmail接受在From报头字段值上不包含域名的地址,通过添加本地域名来“修复”它们。这在大多数企业环境中都能正常工作,但在托管环境中就不行。这种慷慨给邮件提交者的作者解决问题的压力非常小。另一方面,也有人说Sendmail应该接受这个虚假地址,而不是试图纠正它(也就是说,它太保守了,而不是太自由了)。
  • 许多Web浏览器通常都愿意接受不适当的HTML(值得注意的是,许多浏览器接受缺少结束标记的页面)。这可能会导致呈现歧义(结束标记究竟属于哪里?),但是这种不恰当的形式非常常见,已经成为事实上的标准,使得构建任何重要的Web页面都成为一场噩梦。这被称为“规范腐烂”。

所有这些,在某种程度上,都是你在接受事物方面过于自由的例子。这反过来又允许在生成的内容中不够保守的实现永存。

回到顶部

现在该做什么?

那么,该怎么办呢?稳健性原则是错误的吗?从长远来看,在你所创造的东西上保持保守,甚至在你所接受的东西上保持保守,也许会更加稳健。也许有一个中间地带。又或者,稳健性原则只是一堆糟糕选择中最不糟糕的一个。

好吧,其实不是。请记住,在您接受的内容上保持自由是允许协议扩展的一部分。如果每个实现都坚持该字段Y实际上是零,那么推出我们协议的第三版几乎是不可能的。几乎每一个成功的协议都需要在某种程度上进行扩展,要么是因为最初的设计没有考虑到某些东西,要么是因为世界从它下面发生了变化。变化是永恒的,这个世界充满了敌意。标准和这些标准的实施需要考虑到变化和危险。就像所有其他事情一样,稳健性原则必须适度运用。

*致谢

感谢Kirk McKusick提供的第一个InterOp的细节,感谢George Neville-Neil、Stu Feldman和Terry Coatta提供的有用建议。

ACM队列的q戳相关文章
queue.acm.org

深思熟虑的革命
迈克燃烧器
http://queue.acm.org/detail.cfm?id=637960

面向商品企业中间件
约翰·奥哈拉
http://queue.acm.org/detail.cfm?id=1255424

无线网络不可靠
埃里克·奥尔曼
http://queue.acm.org/detail.cfm?id=957735

回到顶部

参考文献

1.又是《关于波斯特尔》(2004年;http://www.tbray.org/ongoing/When/200x/2004/01/11/PostelPilgrim

2.传输控制协议RFC 793,图3 (1981);http://datatracker.ietf.org/doc/rfc793/

回到顶部

作者

埃里克·奥尔曼是Sendmail的联合创始人和首席科学官,Sendmail是最早的开源公司之一。他之前是加州大学伯克利分校猛犸项目的首席程序员。他是INGRES数据库管理项目的首席程序员,并参与了早期在伯克利的Unix工作。奥尔曼是ACM队列编辑顾问委员会。

回到顶部

UT1表格协议图示例。

回到顶部


©2011 acm 0001-0782/11/0800 $10.00

允许为个人或课堂使用部分或全部作品制作数字或硬拷贝,但不得为盈利或商业利益而复制或分发,且副本在首页上附有本通知和完整的引用。除ACM外,本作品的其他组件的版权必须受到尊重。允许有信用的文摘。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,都需要事先获得特定的许可和/或费用。请求发布的权限permissions@acm.org传真(212)869-0481。

数字图书馆是由计算机协会出版的。版权所有©2011 ACM, Inc.


评论


匿名

在11天的NFS修复进程结束时:“在这一点上,大多数(但不是所有)系统都与其他系统相对抗。”

它本身就有一定的模糊性。正确阅读(因为互操作性是双向的过程)“其他”必须指在“大多数系统”的集合中,而通常的意思是“所有系统”。

这篇论文显然不是一个标准,但对我来说,这是一个相关的“案例”。


Anders Kaseorg

太多的Web服务器接受来自用户和其他服务的任意数据,并在没有首先检查数据的合理性的情况下处理它们。这就是SQL注入攻击的原因。

错了。SQL注入攻击的原因是没有使用安全的api和/或缺少适当的转义。如果你的网站编程正确,我没有理由不能给你一个包含撇号(例如沙奎尔·奥尼尔)或任何其他字符的名字,而不会造成问题。

这同样适用于相关的攻击,如跨站点脚本和命令行注入。


匿名

Kaseorg:

我大部分同意。检查和显式转义都是不好的方法。API,最好还有语义和语法(在变量展开的情况下),应该处理这个问题。堆栈中的每一层在设计上都应该是安全的,如果仔细正确地使用,则不安全。失败模式需要一致的努力,而不是松懈的注意力。

SQL注入是供应商的一个失败。任何相信刚从高中或大学毕业的程序员(甚至是有20年经验的程序员)会投入大量精力来避免注入攻击的人,因为通常使用的环境使错误不可避免……他们欺骗。

我做过代码审计,搜索需要显式数据消毒的api和模式的使用。你总是会发现错误;你惩罚冒犯者。但几个月后,同样的错误又悄然而至。责备程序员是没有用的。他们使用的工具往往是设计上的缺陷。


匿名

在第一次SCTP互操作中,我了解到“Rose’s推论到Postel定律”:在任何互操作中,您都希望至少有一个反postelian的实现。在那次互操作中,我们有一个实现,它对它所能接受的内容是绝对严格的——我们解决了每个人的实现的许多问题,甚至还解决了规范中的几个问题


显示所有4评论

Baidu
map