菜单

创造性和防御性编程,PHP中的防御性编程

2020年2月10日 - 前端排行
创造性和防御性编程,PHP中的防御性编程

本文由码农网 –
邱康原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!

 

练习27:创造性和防御性编程

原文:Exercise 27: Creative And Defensive
Programming

译者:飞龙

你已经学到了大多数C语言的基础,并且准备好开始成为一个更严谨的程序员了。这里就是从初学者走向专家的地方,不仅仅对于C,更对于核心的计算机科学概念。我将会教给你一些核心的数据结构和算法,它们是每个程序员都要懂的,还有一些我在真实程序中所使用的一些非常有趣的东西。

在我开始之前,我需要教给你一些基本的技巧和观念,它们能帮助你编写更好的软件。练习27到31会教给你高级的概念和特性,而不是谈论编程,但是这些之后你将会应用它们来编写核心库或有用的数据结构。

编写更好的C代码(实际上是所有语言)的第一步是,学习一种新的观念叫做“防御性编程”。防御性编程假设你可能会制造出很多错误,之后尝试在每一步尽可能预防它们。这个练习中我打算教给你如何以防御性的思维来思考编程。

菲纳格动态逆定律:

 

创造性编程思维

在这个简单的练习中要告诉你如何做到创造性是不可能的,但是我会告诉你一些涉及到任务风险和开放思维的创造力。恐惧会快速地扼杀创造力,所以我采用,并且许多程序员也采用的这种思维方式使我不会惧怕风险,并且看上去像个傻瓜。

我只是暂时接受了这种思维,并且在应用中用了一些小技巧。为了这样做我会提出一些想法,寻找创造性的解决方案,开一些奇奇怪怪的脑洞,并且不会害怕发明一些古怪的东西。在这种思维方式下,我通常会编写出第一个版本的糟糕代码,用于将想法描述出来。

然而,当我完成我的创造性原型时,我会将它扔掉,并且将它变得严谨和可考。其它人在这里常犯的一个错误就是将创造性思维引入它们的实现阶段。这样会产生一种非常不同的破坏性思维,它是创造性思维的阴暗面:

这些都是错误的。你经常会碰到一些程序员,它们对自己创造的软件具有强烈的荣誉感。这很正常,但是这种荣誉感会成为客观上改进作品的阻力。由于这种荣誉感和它们对作品的依恋,它们会一直相信它们编写的东西是完美的。只要它们忽视其它人的对这些代码的观点,它们就可以保护它们的玻璃心,并且永远不会改进。

同时具有创造性思维和编写可靠软件的技巧是,采用防御性编程的思维。

会出错的,终将会出错 —-  在最糟糕的时刻。

 

防御性编程思维

在你做出创造性原型,并且对你的想法感觉良好之后,就应该切换到防御性思维了。防御性思维的程序员大致上会否定你的代码,并且相信下面这些事情:

这种思维方式让你诚实地对待你的代码,并且为改进批判地分析它。注意上面并没有说充满了错误,只是说你的代码充满错误。这是一个需要理解的关键,因为它给了你编写下一个实现的客观力量。

就像创造性思维,防御性编程思维也有阴暗面。防御性程序员是一个惧怕任何事情的偏执狂,这种恐惧使他们远离可能的错误或避免犯错误。当你尝试做到严格一致或正确时这很好,但是它是创造力和专注的杀手。

防御性编程是什么意思

防御性编程,简单的说,就是在编程的时候有目的地预测可能的故障点。目的是在那些可能发生的问题发生前解决它们。你看见了问题,对吧?预测意料之外的事情本来就有内在的难度,当你想要预测意料之外的事情并且解决它就更是难上了好几倍。

下面我们看几个实际的例子。

图片 1

使用断言的最佳时机偶尔会被提起,通常是因为有人误用,因此我觉得有必要写一篇文章来阐述一下什么时候应该用断言,为什么应该用,什么时候不该用。

八个防御性编程策略

一旦你接受了这一思维,你可以重新编写你的原型,并且遵循下面的八个策略,它们被我用于尽可能把代码变得可靠。当我编写代码的“实际”版本,我会严格按照下面的策略,并且尝试消除尽可能多的错误,以一些会破坏我软件的人的方式思考。

永远不要信任输入

永远不要提供的输入,并总是校验它。

避免错误

如果错误可能发生,不管可能性多低都要避免它。

过早暴露错误

过早暴露错误,并且评估发生了什么、在哪里发生以及如何修复。

记录假设

清楚地记录所有先决条件,后置条件以及不变量。

防止过多的文档

不要在实现阶段就编写文档,它们可以在代码完成时编写。

使一切自动化

使一切自动化,尤其是测试。

简单化和清晰化

永远简化你的代码,在没有牺牲安全性的同时变得最小和最整洁。

质疑权威

不要盲目遵循或拒绝规则。

这些并不是全部,仅仅是一些核心的东西,我认为程序员应该在编程可靠的代码时专注于它们。要注意我并没有真正说明如何具体做到这些,我接下来会更细致地讲解每一条,并且会布置一些覆盖它们的练习。

条件语句

这是最容易进行防御性编程的地方之一,也是最容易满足的地方。在用PHP编程的许多情况下你不会需要“else”。

假设,你在写一个函数并且需要一个条件语句。在这里,你只需要为你特定的变量使用三个条件语句如下:

if($var == a){ }
else if($var == b){ }
else if($var == c){ }

没有其他可能性了,你说,并且继续码代码。但是,让我们在这里停一下。我知道你知道这里没有其他可能性了。并且我相信你。但有时候(不可预测的)情况会发生。我们忘掉了一些情况。我们检查错误。我们最终重用了一些代码,超出了原本的预定范围。突然我们有了泄露错误或者有时候是静默的错误状态,因为我们没有使用catch。使用else代码块。在使用switch时要使用default。用它们来返回或者记录错误,这样你才知道发生了什么(如果发生了的话)。虽然会多用两行代码,但当一些你无法预测的事情发生时,这是值得的。

对那些没有意识到用断言的最佳时机的人来说,Python的断言就是检测一个条件,如果条件为真,它什么都不做;反之它触发一个带可选错误信息的AssertionError。如下例所示:

应用这八条策略

这些观点都是一些流行心理学的陈词滥调,但是你如何把它们应用到实际编程中呢?我现在打算向你展示这本书中的一些代码所做的事情,这些代码用具体的例子展示每一条策略。这八条策略并不止于这些例子,你应该使用它们作为指导,使你的代码更可靠。

绝不相信用户输入

你以前有没有听说过这个说法?大多数程序员听过。这有一点含糊,通俗点讲,理所当然。但它是真理。你绝不应该相信用户输入。这不是说你假设所有用户是疯狂的黑客,他们使用一些精心设计的命令来摧毁你的应用。没有必要妄想。但是,你应该假设用户不知道你的代码,他们不知道你需要填写什么参数,或者参数应该多长。他们不知道什么文件类型或者什么大小能上传(即使应用告诉了他们)。偶尔他们会是机器或者黑客并且他们希望在他们的输入中运行脚本,有时候甚至是在登陆后的输入中。你怎么知道你能相信认证或者验证码能在用户输入之前提供一个安全的堡垒?

答案:绝不。

你绝不相信用户输入。如果你信任的用户输入,那么你永远不会有一个突破。明白了吗?所以总是要评估你的输入,一定要保证你在处理数据尤其是要存入数据库或者要把它展示出来时使用了合适的技术。因此 – 绝不相信输入,即使来自不是用户的输入的地方 – 输入验证永远是你的朋友。看看Survive the Deep End: PHP Security 并且使用 validation library.吧。

图片 2

永远不要信任输入

让我们来看一个坏设计和“更好”的设计的例子。我并不想称之为好设计,因为它可以做得更好。看一看这两个函数,它们都复制字符串,main函数用于测试哪个更好。

undef NDEBUG
#include "dbg.h"
#include <stdio.h>
#include <assert.h>

/*
 * Naive copy that assumes all inputs are always valid
 * taken from K&R C and cleaned up a bit.
 */
void copy(char to[], char from[])
{
    int i = 0;

    // while loop will not end if from isn't '\0' terminated
    while((to[i] = from[i]) != '\0') {
        ++i;
    }
}

/*
 * A safer version that checks for many common errors using the
 * length of each string to control the loops and termination.
 */
int safercopy(int from_len, char *from, int to_len, char *to)
{
    assert(from != NULL && to != NULL && "from and to can't be NULL");
    int i = 0;
    int max = from_len > to_len - 1 ? to_len - 1 : from_len;

    // to_len must have at least 1 byte
    if(from_len < 0 || to_len <= 0) return -1;

    for(i = 0; i < max; i++) {
        to[i] = from[i];
    }

    to[to_len - 1] = '\0';

    return i;
}


int main(int argc, char *argv[])
{
    // careful to understand why we can get these sizes
    char from[] = "0123456789";
    int from_len = sizeof(from);

    // notice that it's 7 chars + \0
    char to[] = "0123456";
    int to_len = sizeof(to);

    debug("Copying '%s':%d to '%s':%d", from, from_len, to, to_len);

    int rc = safercopy(from_len, from, to_len, to);
    check(rc > 0, "Failed to safercopy.");
    check(to[to_len - 1] == '\0', "String not terminated.");

    debug("Result is: '%s':%d", to, to_len);

    // now try to break it
    rc = safercopy(from_len * -1, from, to_len, to);
    check(rc == -1, "safercopy should fail #1");
    check(to[to_len - 1] == '\0', "String not terminated.");

    rc = safercopy(from_len, from, 0, to);
    check(rc == -1, "safercopy should fail #2");
    check(to[to_len - 1] == '\0', "String not terminated.");

    return 0;

error:
    return 1;
}

copy函数是典型的C代码,而且它是大量缓冲区溢出的来源。它有缺陷,因为它总是假设接受到的是合法的C字符串(带有'\0'),并且只是用一个while循环来处理。问题是,确保这些是十分困难的,并且如果没有处理好,它会使while循环无限执行。编写可靠代码的一个要点就是,不要编写可能不会终止的循环。

safecopy函数尝试通过要求调用者提供两个字符串的长度来解决问题。它可以执行有关这些字符串的、copy函数不具备的特定检查。他可以保证长度正确,to字符串具有足够的容量,以及它总是可终止。这个函数不像copy函数那样可能会永远执行下去。

这个就是永远不信任输入的实例。如果你假设你的函数要接受一个没有终止标识的字符串(通常是这样),你需要设计你的函数,不要依赖字符串本身。如果你想让参数不为NULL,你应该对此做检查。如果大小应该在正常范围内,也要对它做检查。你只需要简单假设调用你代码的人会把它弄错,并且使他们更难破坏你的函数。

这个可以扩展到从外部环境获取输入的的软件。程序员著名的临终遗言是,“没人会这样做。”我看到他们说了这句话后,第二天有人就这样做,黑掉或崩溃它们的应用。如果你说没有人会这样做,那就加固代码来保证他们不会简单地黑掉你的应用。你会因所做的事情而感到高兴。

这种行为会出现收益递减。下面是一个清单,我会尝试对我用C写的每个函数做如下工作:

只是这些微小的事情就会改进你的资源处理方式,并且避免一大堆错误。

对你的代码的假设

不要假设任何事情。如果前两个主题教会我们一些事情的话,那就是我们不应该做任何假设。作为程序员,尤其是致力于一个项目太久后,我们开始做很多假设。我们假设用户知道一些我们知道的事情。不一定是技术细节,也可以是程序的功能性细节。我们假设用户知道文件能有多大因为。。。我们已经知道。或者他们知道为了让邮件脚本。。。但事实不是,他们不知道以上任何东西。这好像更多的是前端的工作,但明显的是你在后端仍然要处理用户行为和用户输入,所以值得好好想想。

另一个许多程序员都会做的惊人的假设是我们在任何时候对于我们的函数,类或者其它代码段的明显的功能属性。一个具有防御性的程序员会仔细考虑的不仅仅是用一般的文档来描述函数是干什么的——他们也将写下他们对输入,参数,用例,或任何其他类似的东西做出的任何假设。因为我们都是人,我们过一段时间会忘掉一些事。我们最后也很可能会面临其他人维护,扩展或者替换我们的代码。如果没有别的,回想一下,编程是发生在一个充满技术变革的世界里。如果你的应用仍然能使用几年,可能会升级PHP版本并且失去一些功能,或者一些你自己代码里面具有交互的组件之间需要改变。预测这些是很困难的,所以好的注释和文档是非常重要的。

很多人将断言作为当传递了错误的参数值时的一种快速而简便的触发异常的方式。但实际上这是错误的,而且是非常危险的错误,原因有两点。首先,AssertionError通常是在测试函数参数时给出的错误。你不会像下面这样编码:

避免错误

上一个例子中你可能会听到别人说,“程序员不会经常错误地使用copy。”尽管大量攻击都针对这类函数,他们仍旧相信这种错误的概率非常低。概率是个很有趣的事情,因为人们不擅长猜测所有事情的概率,这非常难以置信。然而人们对于判断一个事情是否可能,是很擅长的。他们可能会说copy中的错误不常见,但是无法否认它可能发生。

关键的原因是对于一些常见的事情,它首先是可能的。判断可能性非常简单,因为我们都知道事情如何发生。但是随后判断出概率就不是那么容易了。人们错误使用copy的情况会占到20%、10%,或1%?没有人知道。为了弄清楚你需要收集证据,统计许多软件包中的错误率,并且可能需要调查真实的程序员如何使用这个函数。

这意味着,如果你打算避免错误,你不需要尝试避免可能发生的事情,而是要首先集中解决概率最大的事情。解决软件所有可能崩溃的方式并不可行,但是你可以尝试一下。同时,如果你不以最少的努力解决最可能发生的事件,你就是在不相关的风险上浪费时间。

下面是一个决定避免什么的处理过程:

这一微小的过程会产生一份不错的待办列表。更重要的是,当有其它重要的事情需要解决时,它让你远离劳而无功。你也可以更正式或更不正式地处理这一过程。如果你要完成整个安全审计,你最好和团队一起做,并且有个更详细的电子表格。如果你只是编写一个函数,简单地复查代码之后划掉它们就够了。最重要的是你要停止假设错误不会发生,并且着力于消除它们,这样就不会浪费时间。

视野狭窄

另一件可以使我们忘记好的评论实践以及标准的东西是视野狭窄。许多程序员都具有视野狭窄的毛病。你知道这种感觉 – 你解决问题,你处于最佳状态。你觉得与你的音乐(或没有)独立于自己的小世界中,并且你就在编码,突然两小时过了,你意识到你已经写了无数行没有注释的代码。我们所有人偶尔都会遇到这种事情,但重要的是在某处发现这个情况并且补上应有的注释。

图片 3

过早暴露错误

如果你遇到C中的错误,你有两个选择:

这就是处理方法,你需要执行它来确保错误尽快发生,记录清楚,提供错误信息,并且易于程序员来避免它。这就是我提供的check宏这样工作的原因。对于每一个错误,你都要让它你打印信息、文件名和行号,并且强制返回错误代码。如果你使用了我的宏,你会以正确的方式做任何事情。

我倾向于返回错误代码而不是终止程序。如果出现了大错误我会中止程序,但是实际上我很少碰到大错误。一个需要中止程序的很好例子是,我获取到了一个无效的指针,就像safecopy中那样。我没有让程序在某个地方产生“段错误”,而是立即捕获并中止。但是,如果传入NULL十分普遍,我可能会改变方式而使用check来检查,以保证调用者可以继续运行。

然而在库中,我尽我最大努力永不中止。使用我的库的软件可以决定是否应该中止。如果这个库使用非常不当,我才会中止程序。

最后,关于“暴露”的一大部分内容是,不要对多于一个错误使用相同的信息或错误代码。你通常会在外部资源的错误中见到这种情况。比如一个库捕获了套接字上的错误,之后简单报告“套接字错误”。它应该做的是返回具体的信息,比如套接字上发生了什么错误,使它可以被合理地调试和修复。当你设计错误报告时,确保对于不同的错误你提供了不同的错误消息。

语法和命名的一致性

一致性是一个灰色地带 – 它更多的是关于编码标准之类的,但它和防御性编程也有联系。在PHP中,有标准规范你的代码格式以便别人查看,或者你以后使用。但常常没人让你的代码标准化。但是无论你是否按照标准编码,你至少要保持一致性 – 这能让你少犯错误。这对于需要大量时间返回并且修复的小的语法错误尤其适用。如果你总是使用相同的间隔,格式和语法,命名规格等等你就能更好的避免犯错以至于误读你自己的代码。你更可能快速浏览代码并且找到你需要的东西。

你应该用TypeError来替代,“断言”解决了错误的异常类型。

记录假设

如果你遵循并执行了这个建议,你就构建了一份“契约”,关于函数期望这个世界是什么样子。你已经为每个参数预设了条件,处理潜在的错误,并且优雅地产生失败。下一步是完善这一契约,并且添加“不变量”和“后置条件”。

不变量就是在函数运行时,一些场合下必须恒为真的条件。这对于简单的函数并不常见,但是当你处理复杂的结构时,它会变得很必要。一个关于不变量的很好的例子是,结构体在使用时都会合理地初始化。另一个是有序的数据结构在处理时总是排好序的。

后置条件就是退出值或者函数运行结果的保证。这可以和不变了混在一起,但是也可以是一些很简单的事情,比如“函数应总是返回0,或者错误时返回-1”。通常这些都有文档记录,但是如果你的函数返回一个分配的资源,你应该添加一个后置条件,做检查来确保它返回了一个不为NULL的东西。或者,你可以使用NULL来表示错误,这种情况下,你的后置条件就是资源在任何错误时都会被释放。

在C编程中,不变量和后置条件都通常比实际的代码和断言更加文档化。处理它们的最好当时就是尽可能添加assert调用,之后记录剩下的部分。如果你这么做了,当其它人碰到错误时,他们可以看到你在编写函数时做了什么假设。

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图