您是否应该防止来自外部API的意外值?


51

可以说,您正在编码一个从外部API接收输入的函数MyAPI

该外部API MyAPI的合同规定其将返回stringnumber

它是推荐的防范之类的东西nullundefinedboolean等即使它不属于API的一部分MyAPI?特别是,由于您无法控制该API,因此无法通过诸如静态类型分析之类的方法来做出保证,因此,安全起来总比对不起好。

我在考虑稳健性原则


16
如果返回这些意外值,将不会产生什么影响?您能忍受这些影响吗?处理那些意想不到的值以防止不得不处理这些影响是否值得付出复杂性?
Vincent Savard

55
如果您期望它们,那么根据定义,它们并不意外。
梅森惠勒

28
请记住,API没有义务仅向您提供有效的JSON(我假设这是JSON)。您也可能会收到类似的回复<!doctype html><html><head><title>504 Gateway Timeout</title></head><body>The server was unable to process your request. Make sure you have typed the address correctly. If the problem persists, please try again later.</body></html>
user253751

5
“外部API”是什么意思?它仍然在您的控制之下吗?
重复数据删除器

11
“一个好的程序员就是在横穿单向街道之前会双向看待的人。”
jeroen_de_schutter

Answers:


103

无论来源是什么,您都永远不要相信软件的输入。不仅验证类型很重要,输入范围和业务逻辑也很重要。根据评论,OWASP对此进行了很好的描述

如果不这样做,充其量只能留下您以后需要清理的垃圾数据,但最坏的情况是,如果上游服务以某种方式受到破坏,您将留下被恶意利用的机会(qv目标黑客)。介于两者之间的问题范围包括使应用程序处于不可恢复的状态。


从评论中我可以看到,也许我的答案可能会有所扩展。

所谓“从不信任输入”,只是意味着您不能假设您将始终从上游或下游系统接收有效且可信赖的信息,因此,您应始终尽力清除输入或拒绝输入它。

我将举例说明,评论中出现了一个论点。是的,您必须在某种程度上信任您的操作系统,例如,拒绝随机数生成器的结果(如果您要求输入1到10之间的数字并且它以“ bob”作为响应),这并不是没有道理的。

同样,对于OP,您绝对应该确保您的应用程序仅接受来自上游服务的有效输入。如果情况不佳,您该怎么办,取决于您要完成的实际业务功能,但是您至少要记录下来以供以后调试,否则请确保您的应用程序不会运行进入无法恢复或不安全的状态。

尽管您永远无法知道某人/某事可能给您的所有可能输入,但您当然可以根据业务需求限制允许的输入,并以此为基础进行某种形式的输入白名单。


20
QV代表什么?
JonH

15
@JonH基本上是“也请参阅” ... Target hack是他引用en.oxforddictionaries.com/definition/qv的一个示例 。
andrewtweber

8
这个答案是没有意义的。预期第三方库可能行为不当的每种方式都是不可行的。如果库函数的文档明确保证结果将始终具有某些属性,那么您应该能够依靠它,设计人员确保该属性将实际保留。这是他们的责任有一个测试套件,检查这种事情,并提交万一遇到一种情况,它没有一个bug修复。在自己的代码中检查这些属性违反了DRY原理。
大约

23
@leftaroundabout否,但是您应该能够预测应用程序可以接受并拒绝其余所有有效的事物。
保罗

10
@leftaroundabout不是要不信任一切,而是要不信任外部不受信任的来源。这全部与威胁建模有关。如果您还没有做过,那您的软件就不安全(如果您甚至从未想过要保护应用程序什么样的行为者和威胁,怎么可能?)。对于运行工厂业务软件,假定呼叫者可能是恶意的是合理的默认设置,而假定您的操作系统是威胁则很少明智。
Voo

33

是的,当然。但是,什么使您认为答案可能有所不同?

您肯定不想让程序以某种不可预测的方式运行,以防API无法返回合同规定的内容,不是吗?因此,至少您必须以某种方式应对这种行为。最小限度的错误处理总是值得(非常少!)的努力,并且绝对没有任何理由不执行这样的事情。

但是,应对这种情况您应该投入多少精力在很大程度上取决于大小写,并且只能在您的系统环境中回答。通常,只需简短的日志条目并让应用程序正常结束就足够了。有时,您最好实施一些详细的异常处理,处理不同形式的“错误”返回值,并且可能必须实施一些后备策略。

但是,如果您只编写一些内部电子表格格式的应用程序(供少于10人使用,并且应用程序崩溃的财务影响很小),或者正在创建新的自动驾驶汽车,那将大为不同系统,应用程序崩溃可能会丧命。

因此,没有捷径可以反省自己在做什么,使用常识始终是必不可少的。


该做什么是另一个决定。您可能有故障转移解决方案。在创建异常日志(或死信)之前,可以重试任何异步的内容。如果问题仍然存在,可以选择向供应商或提供者发出警报。
mckenzm

@mckenzm:OP提出一个问题,即字面答案显然只能是“是”,这是恕我直言,这是一个信号,他们可能不只是对字面答案感兴趣。看起来他们在问“是否有必要防止API中不同形式的意外值并以不同方式处理它们”
布朗

1
嗯,胡扯/鲤鱼/死法。传递不良(但合法)请求是我们的错吗?反应是否可能,但特别不适用于我们?还是响应已损坏?在不同的情况下,现在听起来确实像是作业。
mckenzm

21

健壮性原则-具体来说,“一半接受您所接受的东西是自由的”-在软件中是一个非常糟糕的主意。它最初是在硬件的环境中开发的,在物理环境中,物理限制使工程公差非常重要,但是在软件中,当有人向您发送格式错误或不正确的输入时,您有两种选择。您可以拒绝它(最好是对哪里出了问题进行解释),也可以尝试弄清楚它的含义。

编辑:原来我在上面的声明中弄错了。健壮性原则不是来自硬件领域,而是来自Internet体系结构,尤其是RFC 1958。它指出:

3.9发送时要严格,接收时要宽容。在发送到网络时,实现必须严格遵循规范,并容忍来自网络的错误输入。如有疑问,除非规范要求,否则请静默丢弃错误的输入,而不会返回错误消息。

简而言之,这从头到尾都是错误的。出于这篇文章中给出的原因,很难想象出一个比“无提示地丢弃错误的输入而不返回错误消息”更错误的错误处理概念。

另请参阅IETF论文“稳健性原则的有害后果”以进一步详细说明这一点。

永远不要,永远不要选择第二种选择,除非您拥有与Google搜索团队相当的资源来投入您的项目,因为这就是想出一种计算机程序所需的资源,该程序可以在特定的问题域中完成任何体面的工作。(即使那样,Google的建议似乎也有一半时间是直接从左字段出来的。)如果您尝试这样做,最终将是头疼的地方,您的程序将经常尝试解释当发件人的实际意思是Y时,输入为X的错误信息。

这是不好的,有两个原因。显而易见的原因是因为这样您的系统中的数据很差。不太明显的是,在很多情况下,您和发送者都不会意识到出了什么问题,直到很晚以后当您的脸炸毁时,然后突然您要解决一个巨大,昂贵的混乱,并且一无所知出了什么问题,因为至今仍未从根本上消除明显的影响。

这就是为什么存在“快速失败”原理的原因。通过将其应用到您的API,可以节省所有人的头痛。


7
虽然我同意您所说的原则,但我认为您将WRT的健壮性原则误认为是错误的。我从未见过它的意思是“接受坏数据”,只是“不要对好数据过于摆弄”。例如,如果输入是CSV文件,则“稳健性原则”对于尝试以非预期格式解析日期不是有效的参数,但会支持从头行推断列顺序的一个好主意。 。
摩根

9
@Morgen:鲁棒性原则被用来建议浏览器应该接受相当草率的HTML,并导致已部署的网站比如果浏览器需要适当的HTML时要草率得多。但是,问题的很大一部分是,对于人类生成的内容和机器生成的内容使用通用格式,而不是使用单独的人类可编辑和机器可解析格式以及在它们之间进行转换的实用程序。
超级猫

9
@supercat:不过-或正是如此-HTML和WWW非常成功;-)
Doc Brown

11
@DocBrown:很多真正可怕的事情已经成为标准,仅仅是因为当有很多影响力的人需要采用满足某些最低标准的东西时,它们才是第一种可用的方法,而当他们获得牵引力时,为时已晚,选择更好的东西。
supercat

5
@supercat就是这样。例如,马上就会想到JavaScript……
Mason Wheeler,

13

通常,应在可行的情况下构造代码以至少遵守以下约束:

  1. 当给出正确的输入时,产生正确的输出。

  2. 当给出有效输入(可能正确或不正确)时,产生有效输出(同样)。

  3. 当给定无效输入时,对其进行处理时,除了正常输入或定义为发信号错误的副作用以外,没有任何副作用。

在许多情况下,程序实际上将通过各种数据块,而不必特别关心它们是否有效。如果这些块恰好包含无效数据,结果程序的输出可能包含无效数据。除非专门设计用于验证所有数据的程序,并保证即使给定了无效的输入也不会产生无效的输出,否则处理其输出的程序应考虑其中包含无效数据的可能性。

尽管尽早验证数据通常是可取的,但它并不总是特别实用。除其他事项外,如果一个数据块的有效性取决于其他块的内容,并且输入到某些步骤序列中的大部分数据在此过程中是否会被滤除,则将验证范围限制在通过与尝试验证所有内容相比,所有阶段都可能产生更好的性能。

此外,即使程序只是预期给予预先验证的数据,它往往是件好事,它秉承了上述约束反正只要实用。在每个处理步骤中重复进行完整的验证通常会消耗大量的性能,但是为了满足上述约束而需要进行的有限数量的验证可能会便宜得多。


然后,一切都取决于确定API调用的结果是否为“输入”。
马斯托夫

@mastov:对许多问题的答案将取决于如何定义“输入”和“可观察到的行为” /“输出”。如果程序的目的是处理存储在文件中的数字,则可以将其输入定义为数字序列(在这种情况下,不是数字的东西就不可能输入),或者在文件中(在这种情况下,可能出现在文件中可能是一种输入)。
超级猫

3

让我们比较这两种情况并尝试得出一个结论。

方案1 我们的应用程序假定外部API将按照协议运行。

方案2 我们的应用程序假定外部API可能行为不当,因此增加了预防措施。

通常,任何API或软件都有可能违反协议;可能是由于错误或意外情况引起的。甚至API可能在内部系统中也存在问题,从而导致意外结果。

如果我们编写的程序假设外部API将遵守协议,并避免添加任何预防措施;谁将面对这些问题?将会是我们,编写集成代码的人。

例如,您选择的空值。说,根据API协议,响应应为非空值;但是如果突然违反了我们的程序,则会导致NPE。

因此,我相信最好确保您的应用程序具有一些其他代码来解决意外情况。


1

您应该始终验证传入的数据(用户输入或以其他方式输入),因此当从此外部API检索的数据无效时,应该有适当的流程来处理。

一般而言,组织外系统相遇的任何接缝都应要求进行身份验证,授权(如果不是仅通过身份验证简单定义)和验证。


1

通常,是的,您必须始终防止输入错误,但是根据API的类型,“保护”的含义有所不同。

对于服务器的外部API,您不希望意外创建崩溃或破坏服务器状态的命令,因此您必须避免这种情况。

对于诸如容器类(列表,向量等)之类的API,抛出异常是一个很好的结果,损害类实例的状态在某种程度上是可以接受的(例如,提供有错误的比较运算符的分类容器将不可接受)排序),即使使应用程序崩溃也可以接受,但是损害应用程序的状态(例如,写入与类实例无关的随机存储器位置)极有可能不会。


0

提出一个稍微不同的意见:我认为仅使用给定的数据就可以接受,即使它违反了合同。这取决于用法:对于您来说,这一定是字符串,或者只是显示/不使用等。在后一种情况下,只需接受即可。我有一个API,只需要另一个api传递的数据的1%。我不太关心这99%中的数据是什么,所以我将永远不会检查它。

必须在“因为我检查输入不够而出错”和“因为我过于严格拒绝了有效数据”之间取得平衡。


2
“我有一个API,只需要另一个api传递的数据的1%。” 这就提出了一个问题,为什么您的API期望的数据量比实际需要多100倍。如果您需要存储不透明的数据以进行传递,则实际上不必明确其含义,也不必以任何特定格式声明它,在这种情况下,调用方将不会违反您的合同。
Voo

1
@Voo-我的怀疑是,他们正在调用一些外部API(例如“获取X城市的天气详细信息”),然后挑选所需的数据(“当前温度”),而忽略了其余返回的数据(“降雨” “,“风”,“预测温度”,“风寒”等)
Stobor

@ChristianSauer-我认为您与更广泛的共识相差不远-您使用的数据的1%是可以检查的,但是您不必检查的99%则不必。您只需要检查可能会使您的代码崩溃的事情。
Stobor

0

我对此的看法是始终始终检查系统的每个输入。这意味着应该检查从API返回的每个参数,即使我的程序未使用它也是如此。我倾向于检查发送给API的每个参数的正确性。该规则只有两个例外,请参见下文。

测试的原因是,如果由于某种原因API /输入不正确,则我的程序将不依赖任何内容。也许我的程序链接到API的旧版本,该版本与我所相信的有所不同吗?也许我的程序偶然发现了外部程序中从未发生过的错误。甚至更糟的是,无时无刻不在发生,但没人在乎!也许外部程序被黑客欺骗,以返回可能损害我的程序或系统的东西?

测试我的世界中所有事物的两个例外是:

  1. 仔细评估效果后的效果:

    • 在进行测量之前,永远不要优化。与实际调用相比,测试所有输入/返回的数据通常花费很少的时间,因此删除它通常几乎没有节省。我仍然保留错误检测代码,但可以将其注释掉,也许是通过宏将其注释掉。
  2. 当您不知道如何处理错误时

    • 有时候,有时候您的设计根本不允许处理您会发现的那种错误。也许您应该做的是记录错误,但是系统中没有错误记录。几乎总是可以找到某种方式来“记住”该错误,至少允许您作为开发人员在以后进行检查。错误计数器是系统中的一件好事,即使您选择不使用日志也是如此。

究竟如何仔细检查输入/返回值是一个重要的问题。例如,如果说API返回一个字符串,我将检查以下内容:

  • 数据类型是字符串

  • 该长度在最小值和最大值之间。始终检查字符串以获取程序可以预期的最大大小(返回太大的字符串是网络系统中的经典安全问题)。

  • 相关时,应检查某些字符串中的“非法”字符或内容。如果您的程序以后可能发送该字符串以表示数据库,则最好检查数据库攻击(搜索SQL注入)。这些测试最好在系统边界上完成,在此我可以查明攻击的来源,并且可以尽早失败。当以后合并字符串时,执行完整的SQL注入测试可能很困难,因此应该在调用数据库之前进行测试,但是如果您可以尽早发现一些问题,则将很有用。

测试发送到API的参数的原因是为了确保我能获得正确的结果。同样,在调用API之前进行这些测试似乎没有必要,但是性能却很少,并且可能会在我的程序中捕获错误。因此,在开发系统时测试是最有价值的(但如今每个系统似乎都在不断开发中)。根据参数的不同,测试可能会更彻底或更彻底,但是我倾向于发现您通常可以在程序可以创建的大多数参数上设置允许的最小值和最大值。也许字符串应该始终至少包含2个字符,并且最长不能超过2000个字符?最小值和最大值应在API允许的范围之内,因为我知道我的程序将永远不会使用某些参数的全部范围。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.