[译]构建现代Web应用的安全指南

dmc3 9年前

原文: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,甚至在非死book上你可以看到这种事情发生。例如,编辑用户帐户的细节时,你能确保如果用户输入嵌入了另一个用户的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,并改变他的密码。这是不必要的风险。


在邮箱更新时通知旧邮箱:账户侵权之后最常见的行为是改变帐户的电子邮箱,来防止其所有者恢复密码和登录,所以一定要发送一封电子邮件到过去的电子邮箱,在恢复过程添加一个选项。非死book就是这样做的。这招还适用于敏感数据更新。无论是谁在操作,但账户所有者必须被通知。


禁用端口80而不是重定向到443:这样做之后会增大攻击面。如果80端口不需要了,那就禁用它。记住,你的API只应该在443中监听。如果你想从80重定向到443,在<插入CDN名>这个选项处操作。


总是使用通用类的错误信息:记住要始终使用通用的错误信息,例如,在登录尝试时,不要说“用户名无效或密码无效”,只说“证书无效”,让暴力破解更难,虽然可以在注册时枚举电子邮箱,因为你的系统可能会(也应该)让每个帐户的电子邮箱是唯一的。如果你的应用程序产生一个异常,只是说“出错了”,不要暴露异常堆栈。我也建议你使用一些方法来收集所有的异常,并发送到你的邮箱或展现在Raygun, Sentry, Airbrake的dashboard上。


确认用户的电子邮箱或电话:在发送电子邮件或者通知之前要先确认这个邮箱或者电话是否属于该用户。值得推荐的做法是非阻塞法,即让用户可以在没有确认的情况下登录,但这也会影响线上用户的使用。看看非死book:你可以使用未经证实的账户1天。之后,你必须在登录之前确认邮件或电话。我常思考10分钟后邮件失效这样的服务,像上文提到的,好处并不是发送邮件给并不需要它们的用户,而是让你免于被用户标示为垃圾邮件。


其他方面(不排斥其他安全措施)


不要因为供应商有很酷的功能或超低价格就选择他们:你的数据是危险的,那么你的名声也是危险的。有一个叫做“不愿信任”原则,这意味着信任之前你需要小心。减少你的信任也是一件好事。你越相信,就越危险。也就是说,我通常建议安全第一。一开始Bitbucket似乎比GitHub更便宜,但没有两因素身份认证。你的源代码值多少钱呢?AWS引发了公有云市场的竞争;当他们开始关注敏感信息的安全性时,他们似乎做了一件伟大的工作。所以只是在价格便宜的情况下还不足以让我换一个服务商。所有的事情都要被考虑到,但要知道,静态页面接受任何东西,经常会看到企业主页上宣称它们通过APT和SSL(不推荐使用)实现了网站安全。尽量不要轻易相信,当你信任时,先验证!


(REST)面向API的开发:如果你细看AWS,你会发现API是第一位的,然后是web UI,最后是SDKs。API是可怕的,因为它是独立于语言的。但我个人认为,这是将来发展的必然趋势。还值得仔细观察的是HATEOAS。它使各部分之间的可视化隔离变得容易。客户端是静态页面,服务器是接收输入和为前端产生输出的大脑。它能更明确地分离角色和记录,例如web服务器必须验证输入。否则non API的web应用程序更会混乱。


委托办理信用卡:将风险委托给信任的实体是一个好建议。如果你自己去做这件事,就要从一开始就储存信用卡数据,再想一想,这样你要担负多大的责任。如果你委托给可信的支付提供商,如Stripe或PayPal,会不会更好?我觉得会,除非你能做的更好。所以,要确保你的应用不要碰触信用卡数据。可以重定向到他们的网站来完成整个过程。


现在去哪?


有太多的信息了,去搜索吧。OWASP和SANS会给你很大帮助。他们有很多项目、物品、清单和工具。我还建议关注你的工具和供应商方面的安全建议。除此之外,常去Reddit的/r/netsec逛逛。


我很快就会发布一篇关于小团队web应用的开发晚期安全性的文章,敬请关注。



----------------------------------------------

相信爱阅读的您,最近已经注意到了我们。
我们将陆续推出一系列关于业务设计和技术架构方面的好内容。


也欢迎您提出意见、推荐文章,为让别人更了解这个世界,作出自己的贡献。

欢迎任何目的的联系。
我的邮箱:weikan@jointforce.com
我的QQ:3272840549。</span>