原文:Security for building modern web apps
译者:杰微刊—张迪
这篇文章的灵感来自于另一篇文章,它是关于“在今天,构建Web应用之前要知道的事情”的。并不长,但遗漏了一些关于安全性的建议,所以我就此动笔,分享一些这方面的知识。
本文重点是写给那些来自初创公司,并且想要从头开始开发一个Web应用的开发者,他们并不知道太多信息安全的知识,也不想花太多时间考虑其应用程序的安全性。一些重要的内容就不在这里讨论了,诸如威胁建模(threat modeling),持续交付安全(continuous delivery security)等等。这篇文章的目标不是要取代现有的代码安全检查表(例如,OWASP,SANS),而是要从今天的视角补充一下它们。毕竟,安全概念是很老的,(例如,安全设计原则是在70年代被定义的),它会在今天乃至未来都继续存在,所以安全也需要与时俱进,适应现实。
注:虽然像这类的文章都是有益的,但是安全是一个过程,必须从一开始就与开发过程紧密关连。要始终考虑找一个应用安全专家来帮助你。
客户端 Client
输出过滤(Output filtering):著名的跨站点脚本(Cross-Site Scripting),也被称为“XSS”或“HTML注入”,在没有输出过滤和执行某些代码时就会出现问题。防御方法依赖于上下文,如: HTML标签属性上的动态值(onclick、onload等),或标签内部(如,$("p:first").innerHTML=dangerousVariable)。只有在把动态变量存储在HTML标签的属性中时,这种危险代码才会生效。过滤输入对安全会有帮助,但是记住,XSS取决于上下文,所以不是所有的过滤都是有效的。这里有我对XSS的详细解释(PT-BR)。
使用静态页面(Use Static Pages):单页应用程序(SPA)的优点除了由于ajax请求而减少的通信阻塞外,还有就是它拥有一个静态前端。这就意味着有更少的攻击面和更低的成本,因此你可以在Amazon S3上存储你的所有内容,并让Amazon保证其安全,在你没有一个安全技术团队或者你的安全技术团队不如Amazon擅长这个领域的情况下,让Amazon提供安全保证是非常棒的。SPA的缺点是缺乏自定义HTTPS的证书支持(custom https certificate support)。你需要转移到Amazon CloudFront(CDN)上,这很容易实现,它将提升你的web应用的可用性。缺点是需要处理文件夹失效(assets invalidations),但不会太多。他们用一些使用文件名的版本管理技术,虽然很糟糕,但有总比没有好。
避开第三方网站的JSONP反应指令和多种JS文件(甚至广告网络):如果允许第三方网站在你的网站注入JavaScript代码,并且毫无保留地信任它们,结果就是加大了你的网站的被攻击可能性。小心那些响应数据的API,不要让他们轻易被执行。看看Troy Hunt的案例吧。
不要留下HTML注释:有的安全工具可以用于搜索HTML注释,并呈现给攻击者,以查看是否有任何用处,例如OWASP WebScarab。删除HTML注释。如果你需要注释,就在页面生成的时候使用动态语言来添加注释,这些注释就不会出现在响应中了。
客户端校验(服务器端当然也要执行):服务器端校验不能被替代,有两个优点:1)更好的用户体验,因为反馈迅速;2)阻止了后台的无用请求,从而提高有效性。
退出(logout)应在每一个页面都是可见的:请不要忘记这一点。最好是在预期的地方,如点击用户的头像之后的右上角。
如果你将数据存储在客户端,一定要谨慎:相同的威胁适用于移动端,也适用于其他客户存储的设备,会造成数据丢失和被盗。如果你将数据存储在客户端了,就要假设有人会看到它,所以不要存储重要信息。存储就要加密,并把key保存在cookie里(没有可被JavaScript读取到的HTTPOnly标记),至少保存到当前会话结束。当用户注销的时候要删除所有信息。根据数据,你可能想要使用例如HMAC的技术来防止完整性违规(integrity violations)。无论如何,记得这样使用它。当然,服务器中也要保存key。当用于session存储机制时,Rails的cookie会和服务器的APP SECRET一起使用。为了加强这个概念,可以使用Json Web Tokens(JWT),它是目前做这件事(data + signature + algorithm used + expiration + base64 encoding + json format)的标准。
考虑用Json Web Tokens(JWT)取代session:你可以使依赖于JWT的无状态服务器,而不是session和数据库。缺点是保密性差,看上一条就知道了。这个方法可以提高应用的有效性,如果把它们存储在LocalStorage而不是cookie中,还可以防止CSRF攻击。CSRF发生时浏览器无反应(dumbness),即使是跨域请求,cookie也有被传输到服务器的风险。
切记,LocalStorage会受到XSS影响,HttpOnly标识的Cookies则不会:虽然这有利于存储session标识符(cookie w/ HttpOnly标识),但仍有CSRF的风险。这是一个权衡,记住这一条即可。
服务器端 Server
选择一个web框架,至少是MVC:远离构建web应用程序的脚本。最常用的框架已经给了你一些保护(例如,CSRF保护,Security头),如果你正在写PHP,直接使用它们就好了。但是,要小心,你可能会在下面一条跌倒:
避免太过异想天开:我认为这是开发人员中最常见的缺陷。他们对某些有用的功能或框架十分满意,并且盲目地相信它们。这为许多安全漏洞和bug的产生留下了空间。最常见的例子是OAuth库。使用SSO前,一定要了解它的工作细节。否则你会身份验证失败。在开发过程中也没有免费的午餐。在开发之前,在你的应用程序里插入一些未知代码,做一些code review,静态分析,检查已知bug(CVE),并在可能的情况下阅读一下RFC,但是不要盲目地去做,尤其是在web应用程序的关键部分,如身份验证、授权、责任和支付处理/储值卡。
验证CORS源(CORS Origin):除非你打算向整个世界开放API,你应该只允许单页应用的源地址被调用,以避免其他网站的浏览器内(in-browser)调用。
默认设置Cookie标识HTTPOnly:HTTPOnly标识有更多的Cookies是必须的,这能防止Javascript访问cookie值(如会话cookie),这样做能保护Cookies中的信息,即使发生XSS。实际上,恕我直言,HTTPOnly应该是默认属性才对,non-httponly只有在异常中才使用。没有这个标识的cookie仅能用于客户端访问,例如一个根据用户偏好显示或隐藏菜单的标识符。LocalStorage对它的支持也很好,所以我们应该不再使用没有HTTPOnly的Cookie。
默认为Cookie设置secure标示:secure标示允许cookie只能通过HTTPS连接传输,这是伟大的,但你需要有一个HTTPS端口监听工具。如今,它应该是一个必备设置,不仅为了安全,而且为了增加你的谷歌搜索查询排名。据我所知,你不可以在Amazon S3上使用自定义证书。你需要将你的自定义证书部署到Amazon CloudFront(CDN)上,这对你的密钥来说是有害的,但对于小团队来说别无选择。CloudFlare想到了这一点,开发出了无需key的SSL,但你需要建立一个能处理所有SSL握手的服务器,至少是使用这个钥匙的一部分标头,这也意味着需要更多的服务器和更高的成本。
避免业务逻辑Bypass:最常见的缺陷之一就是授权bypass,甚至在facebook上你可以看到这种事情发生。例如,编辑用户帐户的细节时,你能确保如果用户输入嵌入了另一个用户的user_id时,你的应用能够阻止这次更新么?你需要在所有的控制器(controller)上仔细确认。这通常是一些开发人员必须自己实现的验证,所以通常被忽略,或实现得很难看。你自己测试一下,也邀请一个有做安全程序背景的人来测试一下,甚至做一些单元测试来验证你的controller。质量分配漏洞(Mass Assignment Vulnerability)也值得注意,homakov利用它攻击过GitHub。你需要将你的模型参数列入白名单,否则攻击者会通过猜测他们的名字,利用“framework magic”,通过请求参数构建出模型对象。
在你的API中放置CSRF保护: Web框架通常建议你使用CSRF保护,当你构建API时,看到“请求中缺少CSRF token”的消息时,你一般会禁用它之后继续编码。不要那么做。CSRF真的很危险,提醒你自己,确保添加一个CSRF token,即使是在API被调用时。你可以通过以下3种方式做到这一点:
① 有状态session:在每一个session上添加CSRF随机token,检查每一个请求中它们是否匹配。
② 无状态的双Cookie提交技术:攻击者可以操纵请求体(request body),但不能操纵cookies,因为它们来自另一个域,在cookie和请求中向服务器发送相同的随机值,并检查它们是否匹配;如果你的用户(或第三方脚本,如广告)可以控制任何子域,你也有一些技术可以bypass。从Blackhat的文章中得到更多的信息。
③ 无状态的Json Web Token:存储在LocalStorage中,并在每个请求中发送。攻击者不能访问跨域的LocalStorage。
不要让所有操作都获得访问你AWS帐户全部资源的权限:你不会浪费太多时间为你应用的AWS访问凭证找出正确的许可。不要傻到允许访问所有东西。如果你将key上传到一个公共的GitHub库,你就完蛋了,会被攻击,设置权限降低风险吧。
不要将证书存储在源代码里:从源代码部署以外的环境或文件中去读取证书。刚开始会有些麻烦,但一些函数库使它非常容易,如ruby的dotenv gem。
当进行服务端到服务端的通信时,验证端点证书(endpoint),考虑pin它或它的公钥:当你浏览一些HTTPS网站,浏览器会验证其信任的CA。但当你进行从服务端到服务端的通信时,谁来做验证呢?通常没人,所以你需要自己设置逻辑去验证端点证书。验证通过之前,不要允许别的操作,否则SSL/TLS就没意义了。除了在传输过程中加密数据,HTTPS的另一个目标是验证端点的真伪,从而防止中间人攻击。可以考虑使用证书pinning(Certificate Pinning),或者更好的公钥pinning(Public Key pinning)。OWASP有一篇很好的文章详细解释了这一点,所以我不赘述了。最基本的是你只能和你所期待的人交谈,例如,从给定的X509证书中生成一个摘要(digest),并把它与硬编码摘要(hard coded digest)作比较。但是有一个问题,如果证书撤销或者改变,服务将会被拒绝。更好的选择是使用公钥锁定,因为公钥存在于X509证书中,除非证书使用其他密钥对重新生成,否则无论是被撤销还是改变,都可以顺利的通过公钥被验证。这些对移动应用程序也是必须的。
设置安全头(Security Headers):通过在响应中设置安全头,即可保护web应用免遭点击劫持(Clickjacking)、反射型XSS(Reflected XSS)和 IE内容探测(IE content guessing)的攻击(注:如果你发送配置正确,Ruby on Rails能为你做大部分的工作)。更多细节,请查看OWASP页面。
① X-FRAME-OPTIONS:用“否认”或“同源”来防止“点击劫持”。
② X-XSS-Protection:“1;mode=block”迫使XSS反射保护,在Chrome中是默认的, IE中不支持。
③ X-Content-Type-Options:“nosniff”遗憾的是,IE试图猜测web页面的内容,即使这个content/type意味着其他内容类型。如果IE检测HTML代码,它将允许txt文件执行脚本。通过使用这个标头禁用它。
④ Strict-Transport-Security:“max-age=16070400;includeSubDomains”HTTP Strict-Transport-Security(HSTS)保证安全(HTTP通过SSL/TLS)的连接服务器。即使是用户类型的HTTP(user types http),浏览器都将强制HTTPS,这是很棒的。
⑤ 还有其他的,例如Content Security Policy(CSP),就不在这里讨论了。
在“注册”和“忘记密码”页面使用验证码:多亏了谷歌的reCaptcha,如今的验证码已经不是很烦人了。今天,你可以验证用户是否是基于他的行为而不仅仅是人类挑战,从而防止假账户和疯狂的发送电子邮件。
存储API密钥就像你存储密码一样(或尽可能这么做):如果双方泄漏的影响是相同的,那么为什么储存一个比另一个更安全?实际上是有一些不同之处的,但关键是不要在明文中存储API密钥。API密钥应该是系统生成的随机字符,所以他们不会受到字典攻击(dictionary attack),就像密码,但是,在数据库/文件系统/ OS中,API密钥将在未经加密的文字或数据中可用。也就是说,至少一些hash是必要的。如果你使用像scrypt或BCrypt这样的工具,你就要小心了。scrypt或BCrypt因为其缓慢的哈希计算,非常建议用于密码。缓慢的哈希计算也会导致服务被拒绝。你输入一次密码,得到一个session ID;但是API就不同了,API验证时刻都要被调用,所以速度缓慢会降低应用的可用性。存储API Key的摘要,足够满足你的使用了SHA256或SHA512算法的应用了。远离MD5和SHA1。一定要远离!
至少为用户使用UUID作为主键,而不是顺序ID:防止用户帐户的猜测/暴力破解和轻易复制。有更多的优势和少数劣势,但它是值得的。注意:相较于连续整数,UUID并不会使应用更安全,仅是从安全的维度增加了不可法猜测性和模糊度。
忘记密码和电子邮件确认的token:为忘记密码或电子邮件确认生成一个token时,请确保使用安全的伪随机数生成器(RPNG),否则可能被猜到。使用可以信任的库/语言API。也为这个token设置截止日期/时间。设想一下使用情景,用户不想改变自己的密码,但一周后,有人拦截了电子邮件,访问了那个URL,并改变他的密码。这是不必要的风险。
在邮箱更新时通知旧邮箱:账户侵权之后最常见的行为是改变帐户的电子邮箱,来防止其所有者恢复密码和登录,所以一定要发送一封电子邮件到过去的电子邮箱,在恢复过程添加一个选项。Facebook就是这样做的。这招还适用于敏感数据更新。无论是谁在操作,但账户所有者必须被通知。