一个系统登录实例代码的分析和思考(反面教材)

177 阅读11分钟

缘起

某日某时,笔者在检查和分析和自身业务相关的一个参考系统的前端代码的时候,看到了下面这段核心代码:

lcode.png

看了这段代码,不知道读者的感受如何。我们也不去评判这段代码是如何写出来的,或者从哪里借鉴而来的(因为笔者觉得这个开发者基本上不理解为什么要这么做)。笔者觉得这段代码还是很有意思,很有代表性,可以看到很多普通开发者在编写代码过程中的问题,对HTML,JS,密码学等概念和理解的缺陷。

针对这种情况,本文尝试对这些问题分类,并给出相关的分析内容,还会进一步给出改进的建议,希望读者在这些方面有所收获。当然,这只是核心代码,我们也只讨论相关的核心问题,还有一些相关的外围辅助代码和函数,笔者会进行解释和说明,应该不影响我们的讨论。

基本流程

先简单分析一下,这段操作的基本过程是:

  • 这是一个登录执行的函数,在被调用时,会执行一些数据处理,最后一步是提交一个表单
  • 首先,进行一个检查,调用check(),应该是检查登录用户名、密码、验证码等内容和规则
  • 设置form的action目标url地址,就是提交地址
  • 从表单元素中取用户名
  • 并对用户名进行编码加密
  • 将隐藏用户名输入框内容设为加密内容
  • 将用户名输入框清空
  • 从表单元素取密码值
  • 对密码加盐(连接密码值和一个盐变量的值)
  • 加盐后进行md5编码
  • 对编码后的密码,进行加密
  • 设置密码数据框内容为加密后的内容
  • 提交表单

下面我们就尝试对这个流程和相关的操作,分类进行说明。

表单和表单元素

首先,如果不是动态的表单,在函数过程中,将表单元素从DOM中取出,并赋予一个变量就是不合适的。当然这个方法,正常情况下确实只会被调用一次(登录成功和失败,页面都会刷新重置),但将固定赋值,放在可能会被反复调用的函数体中,就会被反复执行,是不合适的。同理也适合于设置表单action的情况。

可能的一种比较好的方式,是在页面加载的时候,就将要使用的页面元素赋值给某个常量。这样可以告知浏览器对对象赋值和使用进行优化,另外,如果这个对象如果在其他的函数中也要使用的话(比如这里的check方法,肯定也会用到这些表单元素),就不用再次赋值或者查找了。

可以想象,开发者在前面的页面中,使用名字,来定义的表单元素,这里也许如果确定就使用特定的DOM元素,可以并应该使用ID和getElementById方法。

最后,对于表单对象也是,这里直接使用了documents.forms[0]这种形式,很容易出错,不便于维护。可以使用通用的方法,给其赋予一个ID,并预先准备好这个表单变量。

其实,上面说到的表单元素的处理,只是小问题。这段代码的核心问题是在于开发者对于密码学的理解和操作方面的不足。

密码学操作

就代码而言,开发者对于信息安全和密码学还是有一定概念的。也遵循了一些基本的信息安全和数据操作原则。比如不直接使用密码原文或者加密,而是使用摘要信息作为验证信息;无论是否使用HTTPS,都对用户名和密码信息进行了加密等等。但,好像仅此而已,或者说,他虽然进行了这些操作,但好像不知道为什么要这样操作。所以,在这里犯了不少似是而非的错误。

首先是选择的算法,结合其使用的密码库,开发者应该是使用了MD5作为摘要算法,使用了3DES作为加密算法。看起来像是前互联网时代的产品,就算这个应用系统应该差不多是2010年左右开发的,这个技术选择,也是不应该的。有两个原因,第一个是MD5和DES过于古老,并且已经被证明不够安全;第二是它们都有对应的替代技术,并且已经成为行业标准,并且已经大规模广泛使用。所以,在这里,摘要算法和对称加密应该选择使用SHA2和AES,再具体一点,可以推荐使用SHA256和AES-256-GCM(或者Chacha20-Poly1305)。

第二,是关于盐(Salt)的使用。一般情况下,使用salt是为了在密码学计算过程中,加入某种随机性,所以盐首先应该是一个随机信息,其次盐并不是密码,是可以公开的。在本案例中,开发者使用盐参与了摘要计算,我们可以理解为他想用盐来混淆摘要结果。问题是这里使用了固定的盐值,就没有达到随机化的目的,对于固定的密码,摘要值也只能是一个固定的值。也许有人可能会说,这里并不是作为标准盐信息来使用,而是当作一个共享秘密信息来混淆摘要,但作为一个常量,写在实现代码里? 笔者不知道他在后端是如何处理的,正常情况下,密码都是用这个固定的验证进行摘要的,这样,其实提供了一种相对更简单的暴力破解计算的可能,并且抵御重放攻击的能力也有所下降。

顺便说一下,就算想用密钥对摘要进行混淆,常规的做法,也不是直接拼接内容和密钥(存在可能的偏置攻击方式,早期的支付宝算法就有这个问题),而是使用HMAC算法(笔者另有文章有详细阐述)。

第三,最大的问题,如果笔者没有理解错的话,是这个开发者,将3DES算法的密钥,写在了代码里面!并且通过对页面URL和内容的分析,这还是一个固定值。作为攻击者,只需要分析页面代码,连侦听解惑认证通信流量的过程都省了。所以,可以不客气的说,虽然表面上开发者使用了加密,但其实他不知道自己在做什么。

第四,在认证界面中,其实设计了一个验证码的机制的。如果没有理解错误的话,他在check函数中检查了验证码信息不为空,但直接提交这个表单中的验证码信息使用的就是原文,并没有做摘要或者加密处理。验证码的核查,也应该是使用session机制来实现的。这些都可能会影响到认证过程的安全性。

第五,就是作为一个国家级应用系统,在前面已经有那么多问题的情况下,仍然没有使用HTTPS部署方式,这里笔者已经不想再说什么了。

综上所述,如果要笔者在现有的情况下,提供改进意见的话,可能的建议和技术方案如下:

  • 使用HTTPS
  • 改进加密摘要算法,使用MHAC-SHA256,并使用随机信息作为KEY
  • 改进加密算法,使用AES-256-GCM
  • AES加密密钥,应当由非对称加密算法协商而来
  • 如果实在无法做到非对称密钥协商的话,也应该使用服务端动态或定期生成密钥,客户端获取并使用的方式。
  • 验证码的获取、验证码文本请求和验证,可以不使用session的方式,减少资源占用

当然,笔者觉得最好的方式还是使用认证AJAX请求和认证API接口的方式,这就是另外一个问题了。

关于登录操作的探讨

在明确了文中代码的问题和改进方案后,笔者还想借此机会,进一步来探讨“登录”这种行为和操作,以及可能需要注意的问题,特别是安全性方面的。由于篇幅的限制,加之笔者已经另有撰文有详细的技术分析,这里的讨论主要是概念性和原则性的。

  • 认证信息

所谓认证,其实质是一种技术过程,让认证方,可以通过某种方式,对被认证方予以认可。认证可以有很多实现方式,最常见的就是用户名+密码的方式。这里面其实有两个层面的问题,第一被认证方有一个身份的声明(我是谁),就是提供用户名(账号),将自己和其他用户区隔开来;第二就是可以证明这一点(我可以证明我是谁),最常用的方式就是预先双方约定这个账号的密码,如果认证时,被认证方可以提供这个密码,认证方对比密码就可以完成认证。

逻辑上是这样,但如果考虑安全性的话,有很多问题。所谓秘密,其实其逻辑内核就是只关联单一实体,如果有多方都知晓,其实就谈不上什么秘密可言,因为这种情况会导致信息泄露的不可追溯性和责任混乱。所以,和一般的理解不同,我们应该认为认证方,是不应该知道这个秘密的具体内容是什么的,他只需要能够通过某种方式,来验证认证方知晓这个秘密就可以了。

可以举一个可以在现实生活中理解的例子,被认证者,需要向认证方,证明自己有房间的钥匙。常规的方式,是认证方也需要有这把钥匙的副本并且进行比较吗?或者进一步需要看着被认证者用钥匙打开房门吗? 其实可能有更好的方式,比如可以写一张字条放在房间里,认证时,可以让被认证者说出字条的内容就可以了。这个在逻辑上,也能证明其确实有房间的钥匙。这个方式通常也被称为“零知识证明 (Zero-Knowledge Proof)”。

在具体实现方式上,可操作性比较强的通常有两种选择。第一个就是对密码进行加盐的摘要,然后将摘要作为认证信息,考虑到可能在认证端存储的本身就是摘要信息,可以嵌套摘要和加盐的计算;第二就是挑战相应,这需要多一次交互,收到认证请求后,认证端向被认证方发送一个随机信息,在认证端可以使用密码计算出一个认证信息,提交认证端检查此认证信息来确认认证方知晓此密码。

  • 认证过程

在信息安全中有一个“纵深防御”的概念,就是可以通过简单的叠加安全措施,就可以大幅度提高系统或者过程的安全性。因为如果要对这个多重体系进行破解和攻击,攻击者可能需要进行深入细致的分析、设计、组合甚至需要开发定制特定的攻击方案,从而降低了大规模自动化攻击的经济效益,导致成本过高并可能放弃攻击。

因此,在设计认证过程中,可以平衡技术实现和安全要求,综合考虑前端实现、计算性能、传输性能、后端实现、交互过程和存储等多方面的情况,尽可能的达到更高的安全性。具体而言,可以在实现方案中考虑并遵循以下基本原则:

. 服务端不保存密码和其直接编码(如保存摘要也需要加盐)

. 前端不使用任何固定的认证相关信息,如固定的盐、密钥或者附加配置信息等等

. 认证过程中,传输的数据尽量动态化、随机化

. 关键认证信息,最好加密,特别是用户标识,为了减少攻击面,应该加密传输

. 限制认证过程的有效时间,抵抗重放攻击

. 使用某种机制,抵抗程序化认证的请求(如使用CAPTCHA并结合认证过程)

. 尽量选择主流的密码学相关算法和配置方式

. 大规模的服务端认证,必须考虑密码学信息处理的性能和效率

如果能够实现遵循上面原则的,我们就能够认为是一个比较理想的认证过程,能够达到一个比较安全的状态。