资讯专栏INFORMATION COLUMN

[译] Martin Fowler - Web 应用安全基础

yanest / 1155人阅读

摘要:浏览器的同源策略并不能够避免来自于恶意站点的提交。为了保证输入数据的完整性,服务器端务必要进行数据校验。输入验证即是保证实际输入与应用预期的输入的一致性。因此验证输入时保证系统安全性与防卫危险的第一道防线。

Github Repo:https://github.com/wxyyxc1992/infosecurity-handbook/blob/master/Reinforce/WebSecurity/basics-of-web-application-security.md
原文:The Basics of Web Application Security

现代的软件开发者已经有点像瑞士军刀了,首先,你需要来保证完成用户的功能或者业务需求,并且要保证又快又好地完成。其次,你希望你的代码能够拥有充分的可理解性或者可扩展性:能够随着IT需求的快速变迁而有着充分的扩展空间,与此同时还需要稳定与可用。开发者必须列举出有用的接口,优化数据库,以及频繁地建立或者维护一个交付渠道。不过,当我们审视这长长的需求列表的时候,在快速、低成本以及灵活可扩展之下的,即是安全性。或许直到一些东西出了问题,或者你构建的系统被攻击了之后才能深刻感受到安全才是最重要的。安全这个概念有点像性能,是个泛化的跨越了多个领域的概念。所以一个开发者怎么才能在模糊的安全需求与未知的风险面前选择合适的开发规划呢?当然如果能够明确这些安全需求与定位到威胁的话毫无疑问非常值得推荐,但是这个准备本身就需要耗费大量的时间与金钱。

Trust(信赖)

首先,在讨论具体的输入输出之前,我们需要来强调下自认为安全中最重要也是最根本的原则:Trust。作为一个开发者,也需要不断地问自己,我们相信来自于用户浏览器的请求吗?我们相信上游系统正常工作来保证了我们数据的干净与安全吗?我们相信服务器与浏览器之间的信道就不会被监听或者伪造吗?我们相信我们系统本身依赖的服务或者数据存储吗?呵呵,都不可信。

当然,就像安全一样,Trust也不是一个双选题,非黑即白。我们需要明白系统的风险忍受力与数据的安全边界。为了能够正确的、基于某个统一规则的预估,我们需要审视威胁与风险,这个评估方法与标准会在另一篇文章中讲解。

Reject Unexpected Form Input(拒绝未知的表单输入)

HTML表单本身就可能带来些好像很安全的错觉,表单的构建者肯定觉得他们限制了输入类型、做了数据校验,这样整个表单输入就是安全的。但确信无疑的是,这只是个错觉,尽管客户端地JavaScript脚本可以从安全地角度来说提供完整的校验。

Untrusted Input

无论我们是否在客户端提供了表单验证或者是否使用了HTTPs连接,我们能够信赖来自用户浏览器的连接的比例都是0。用户可以轻易地在发送之前修改标记,或者使用类似于curl这样的命令行来提交没有经过校验的数据。乃至于一个不明所以的用户可能在一个怀有恶意的网站莫名其妙地添了些内容。浏览器的同源策略并不能够避免来自于恶意站点的提交。为了保证输入数据的完整性,服务器端务必要进行数据校验。

不过估计有人有疑问了,为啥说这个畸形的数据就会导致安全问题呢?这往往取决于你的应用业务逻辑与输出的编码,为了避免不可预知的行为、数据泄露与潜在攻击,需要在输入的数据与可执行代码之间架构一个过滤层。譬如,我们的表单里有一个选择的按钮来允许用户选择合适的通信类型,我们的业务逻辑代码可能是这样的:

final String communicationType = req.getParameter("communicationType");
if ("email".equals(communicationType)) {
    sendByEmail();
} else if ("text".equals(communicationType)) {
    sendByText();
} else {
    sendError(resp, format("Can"t send by type %s", communicationType));
}

上面代码危不危险取决于sendError这个方法是怎么定义的,而我们肯定无法确定下游的代码就一定是安全的。最好的选择就是我们在控制流中移除这个危险,而使用的方法就是输入验证。

Input Validation

输入验证即是保证实际输入与应用预期的输入的一致性。超出预期的输入数据会导致我们系统抛出未知的结果,譬如逻辑崩坏、触发错误乃至于允许攻击者控制系统的一部分。其中像数据库查询这样的能够在服务端作为可执行代码的输入与JavaScript这样在客户端能够被执行的代码更是特别的危险。因此验证输入时保证系统安全性与防卫危险的第一道防线。

开发者们在构建应用系统的过程中会进行一些基本的验证,譬如判断值是否为空或者是否为正数。而从安全的角度考虑,我们需要将输入限定到系统允许的最小集合中,譬如数值型值可以被限定在某个特定的范围内。譬如,系统不会允许用户将一个负值添加到购物车中。这种限制性的验证手段就是所谓的positive validation或者whitelisting。一个白名单可以用于限定某个具体的URL或者yyyy/mm/dd这样的时间日期。它可以限制输入的长度、单个字符的编码规范或者上面例子中的只有给定值可以被接受。

另外一种考虑输入验证的思维角度就是把它当做服务端与消费者之间签订的一种协议,任何违背了这个协议的请求都是无效的并且被拒绝。你的这个协议越严格,你的系统在未知情况下遭受的风险就会越小。而当对于某个输入验证失败之后,开发者也要好好考虑应该如何反馈。最严格,也是最有争议的办法就是全部拒绝,并且没有任何反馈,不过要注意将这个事情通过日志或者监控记录下来。不过为啥一点反馈都没有呢?我们需要提供给用户哪些信息是无效的吗?这一点还是要取决于你的约定。在上面的例子中,如果你接收到了除了email或者text之外的内容,那你有可能被攻击了。不过如果你进行了反馈,可能正中全套。譬如如果开发者直接返回:俺们并不认识你传入的communicationType,可能这个还无伤大雅,但是如果是这样的呢:

这种情况下你就会面临一个用来盗取你的Cookies的XSS攻击代码,如果你一定要给用户反馈,你必须保证不会把不受信任的用户内容直接返回,而应该使用固定的提示信息。如果你不可避免地要把用户的输入反馈回去,你要保证它是被编码的。

In Practice

实践中,我们经常要通过过滤

为了便于分析这个漫画所要表达的含义,我们假设这个成绩追踪系统有一个用于增加新的学生信息的函数:

void addStudent(String lastName, String firstName) {
        String query = "INSERT INTO students (last_name, first_name) VALUES (""
                + lastName + "", "" + firstName + "")";
        getConnection().createStatement().execute(query);
}

如果输入的参数是"Fowler"与"Martin",那么最终构造出的SQL语句为:

INSERT INTO students (last_name, first_name) VALUES ("Fowler", "Martin")

不过如果输入的是上面那娃的名字,那么整个待执行的SQL语句就变成了:

INSERT INTO students (last_name, first_name) VALUES ("XKCD", "Robert’); DROP TABLE Students;-- ")

实际上,这个SQL语句一共执行了两个操作:

INSERT INTO students (last_name, first_name) VALUES ("XKCD", "Robert")

DROP TABLE Students

最后的--注释是为了屏蔽余下的内容,保证整个SQL语句能够稳定执行。类似于这样的攻击载荷能够执行任意的SQL语句,换言之,攻击者能够在数据库内像这个应用系统一样做任何事情。

采用参数绑定来解决这个问题

对于上文描述的这种场景,如果只是依赖于简单的清洗过滤,肯定无法应付所有的攻击载荷,这也不是一个正道。基本上能够采取的方法就是所谓的参数绑定,譬如JDBC中提供的PreparedStatement.setXXX()方法,参数绑定可以将像SQL这样的可执行代码与需要进行编码、过滤的内容区分开来:

void addStudent(String lastName, String firstName) {
        PreparedStatement stmt = getConnection().prepareStatement("INSERT INTO students (last_name, first_name) VALUES (?, ?)");
        stmt.setString(1, lastName);
        stmt.setString(2, firstName);
        stmt.execute();
 }

一般来说,一个功能比较全面地数据访问层都会提供这种参数绑定的功能,开发者在开发的时候就要注意将所有的不受信任的输入通过参数绑定生成SQL语句。

Clean and Safe Code

有时候我们开发时会遇到一个两难的问题,即是好的安全性与干净整洁的代码之间的冲突。为了保证安全性往往需要我们增加些额外的代码,不过在上面的例子中我们还是同时达成了较高的安全性与好的代码设计。使用绑定的参数不仅能使应用系统免于注入攻击,还能通过在代码与内容之间构建清晰的边界来增加整个代码的可读性,并且与手动拼接相比还能大大简化构造可用的SQL的过程。当你用参数绑定来代替原本的格式化字符串或者字符串拼接来构造SQL的时候,你会发现还能用全局的绑定方程来完成这一工作,这又会大大增加整个代码的整洁度与安全性。

Common Misconceptions

有一个常见的错误思维就是觉得存储过程能够避免SQL注入攻击,但是这个只有在你是通过参数绑定的方式传入参数的情况下。如果存储过程本身也是用的字符串连接的方式,那么同样存在SQL注入攻击的风险。类似的,像ActiveRecord、Hibernate或者.Net Entity这样的框架,也是只有在用参数绑定来构造SQL的情况下才会进行SQL注入清洗。

最后,还有一个常见的错觉就是NoSQL数据库不会被SQL注入攻击影响。这肯定是不对的,所有的查询语言,无论是不是SQL都需要在可执行代码与输入的内容之间划定明晰的边界来防止参数混淆可执行的命令。攻击者会不停寻找能够在运行时打破这种边界隔离的方法从而进行潜在的攻击。即使是Mongodb,采用了二进制的协议与多种语言特定的API都会存在被注入的风险,譬如$where这个操作符。

Parameter Binding Functions
Framework Encoded Dangerous
Raw JDBC Connection.prepareStatement() used with setXXX() methods and bound parameters for all input. Any query or update method called with string concatenation rather than binding.
PHP / MySQLi prepare() used with bind_param for all input. Any query or update method called with string concatenation rather than binding.
MongoDB Basic CRUD operations such as find(), insert(), with BSON document field names controlled by application. Operations, including find, when field names are allowed to be determined by untrusted data or use of Mongo operations such as "$where" that allow arbitrary JavaScript conditions.
Cassandra Session.prepare used with BoundStatement and bound parameters for all input. Any query or update method called with string concatenation rather than binding.
Hibernate / JPA Use SQL or JPQL/OQL with bound parameters via setParameter Any query or update method called with string concatenation rather than binding.
ActiveRecord Condition functions (find_by, where) if used with hashes or bound parameters, eg: where (foo: bar)where ("foo = ?", bar) Condition functions used with string concatenation or interpolation: where("foo = "#{bar}"")where("foo = "" + bar + """)
In Summary

避免直接从用户的输入中构建出SQL或者等价的NoSQL查询语句

在查询语句与存储过程中都使用参数绑定

尽可能使用框架提供好的原生的绑定方法而不是用你自己的编码方法

不要觉得存储过程或者ORM框架可以帮到你,你还是需要手动调用存储过程

NoSQL 也存在着注入的危险

Protect Data in Transit

当我们着眼于系统的输入输出的时候,还有另一个重要的店需要考虑进去,就是传输过程中数据的保密性与完整性。在使用原始的HTTP连接的时候,因为服务器与用户之间是直接进行的明文传输,导致了用户面临着很多的风险与威胁。攻击者可以用中间人攻击来轻易的截获或者篡改传输的数据。攻击者想要做些什么并没有任何的限制,包括窃取用户的Session信息、注入有害的代码等,乃至于修改用户传送至服务器的数据。

我们并不能替用户选择所使用的网络,他们很有可能使用一个开放的,任何人都可以窃听的网络,譬如一个咖啡馆或者机场里面的开放WiFi网络。普通的用户很有可能被欺骗地随便连上一个叫免费热点的网络,或者使用一个可以随便被插入广告的网路当中。如果攻击者会窃听或者篡改网路中的数据,那么用户与服务器交换的数据就好不可信了,幸好我们还可以使用HTTPS来保证传输的安全性。

HTTPS and Transport Layer Security

HTTPS最早主要用于类似于经融这样的安全要求较高的敏感网络,不过现在日渐被各种各样的网站锁使用,譬如我们常用的社交网络或者搜索引擎。HTTPS协议使用的是TLS协议,一个优于SSL协议的标准来保障通信安全。只要配置与使用得当,就能有效抵御窃听与篡改,从而有效保护我们将要去访问的网站。用更加技术化的方式说,HTTPS能够有效保障数据机密性与完整性,并且能够完成用户端与客户端的双重验证。

随着面临的风险日渐增多,我们应该将所有的网络数据当做敏感数据并且进行加密传输。已经有很多的浏览器厂商宣称要废弃所有的非HTTPS的请求,乃至于当用户访问非HTTPS的网站的时候给出明确的提示。很多基于HTTP/2的实现都只支持基于TLS的通信,所以我们现在更应当在全部地方使用HTTPS。

目前如果要大范围推广使用HTTPS还是有一些障碍的,在一个很长的时间范围内使用HTTPS会被认为造成很多的计算资源的浪费,不过随着现代硬件与浏览器的发展,这点计算资源已经不足为道。早期的SSL协议与TLS协议只支持一个IP地址分配一个整数,不过现在这种限制所谓的SNI的协议扩展来解决。另外,从一个证书认证机构获取证书也会打消一些用户使用HTTPS的念头,不过下面我们介绍的像Let"s Encrypt这样的免费的服务就可以打破这种障碍。

Get a Server Certificate

对于网站的安全认证依赖于TLS的底层的支持,如果客户端只是根据网站说它自己是谁就是谁,那么攻击者可以轻易的使用中间人攻击来模拟站点,从而绕过所有协议提供的安全机制。在使用了TLS协议之后,一个网站可以用它的公钥证书来证明它自己是谁。在某些系统中客户端也需要用公钥证书证明自己是谁,不过大部分情况下受限于为用户管理证书的复杂性,这个并没有广泛使用。除非一个网站证书的真实性已经经过了验证,不然客户端在收到一个证书的时候也要通过一定的手段来验证证书的真实性。而在Web浏览器或者其他的应用中,往往是通过一个第三方的称作CA的机构来管理证书并且提供验证功能,包括验证这个证书与证书所属网站的真实性。

如果我们通过其他渠道已经能够提前得知某个证书是否可信,那也就没必要再经过第三方机构进行仲裁。譬如一个移动APP或者其他应用在分发的时候就会内置一些证书从而在使用时来验证站点是否真实可信。大部分关于HTTPS是否可信的指示会在浏览器访问某个HTTPS的站点的时候显示出来,如果没有的话浏览器会显示一个告警信息来警告用户不要访问不可信的站点。

在测试的时候我们可以自己创建配置一个证书用于HTTPS认证,不过如果你要提供服务给普通用户使用,那么还是需要从可信的第三方CA机构来获取可信的证书。对于很多开发者而言,一个免费的CA证书是个不错的选择。当你搜索CA的时候,你可能会遇到几个不同等级的证书。最常见的就是Domain Validation(DV),用于认证一个域名的所有者。再往上就是所谓的Organization Validation(OV)与Extended Validation(EV),包括了验证这些证书的请求机构的信息。虽然高级别的证书需要额外的消耗,不过还是很值得的。

Configure Your Server

当你申请到了证书之后,你就可以开始配置你的服务器支持HTTPS了。虽然HTTPS啊,包括TLS/SSL的原理好像要个密码学的PHD学位才能理解,但是要把他们配置着用起来还是很容易的呦。不同的加密算法与站点使用的协议的版本差异会大大影响到它能够提供的通信的安全级别。所以咋才能一方面保证我们站点的安全性另一方面又保证那些使用老版本的浏览器的用户也能正常使用网站服务呢?这里要推荐下Mozilla提供的Security/Server Side TLS工具,可以协助来自动创建适用的Web服务器的配置。

Use HTTPS for Everything

现在我们经常碰到一些网站仅仅只用HTTPS来保护部分资源,有些情况下只会保护一些对于敏感资源的提交操作。另一些情况下,部分网站只会将HTTPS用于自认为敏感的资源上,譬如一个用户登录之后才能见到的东西往往是HTTPS加密的。而现在的麻烦事还有很多站点并未使用HTTPS来保护自己,导致整个网站还处于被中间人攻击的危险之下。笔者很是建议这类网站应该直接关闭掉HTTP端口从而强制性让用户转到HTTPS,虽然这并不是一个理想的解决方案,不过估计是最好的解决方法。

如果是在Web浏览器中呈现的资源,那可以添加一个HTTP请求转发的配置,来将所有的HTTP请求转发到HTTPS端口上,譬如:

# Redirect requests to /content to use HTTPS (mod_rewrite is required)
RewriteEngine On
RewriteCond %{HTTPS} != on [NC]
RewriteCond %{REQUEST_URI} ^/content(/.*)?
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [R,L]
Use HSTS

让用户从HTTP迁移到HTTPS可以来避免使用原始的HTTP请求带来的风险。为了帮助站点把用户从HTTP迁移到HTTPS,现代的浏览器支持一个非常强力的安全特征叫做HSTS(HTTP Strict Transport Security),来告诉浏览器我这个站点只会接收来自于HTTPS的请求。这个特性最早来自于2009年的Moxie Marlinspike"s提出的一个用于演示基于HTTP的潜在危险的SSL剥离攻击。可以用如下的设置来启用这个特性:

Strict-Transport-Security: max-age=15768000

上述的设置会告诉浏览器只和使用HTTPS的站点进行交互,HSTS是一个非常重要的强制使用HTTPS的特性。一旦开启之后,浏览器会自动把不安全的HTTP请求切换到HTTPS,尽管用户没有显式的输入"https://"。而在浏览器端开启HSTS特性只需要添加如下的一行代码:


    ...

    # HSTS (mod_headers is required) (15768000 seconds = 6 months)
    Header always set Strict-Transport-Security "max-age=15768000"

不过现在并不是所有的浏览器都支持HSTS特性,你可以通过访问 Can I use. 来看看你面向的用户常用的浏览器能不能使用。

Protect Cookies

浏览器目前有内建的安全机制来避免包含敏感信息的Cookie暴露出来。在Cookie中设置secure标识位能够强制让浏览器只会用HTTPS来传递Cookie,如果你已经使用了HSTS也要记得这样设置来保护Cookie。

Other Risks

即使你全站都用了HTTPS,也还是有几个地方可能导致敏感信息的泄露的。譬如如果你直接把敏感数据放在URL里面,然后这个敏感的URL又被缓存在了浏览器的历史记录里。除此之后,如果包含了敏感信息的站点被链接到了其他的网站中,那么在用户点击链接之后整个敏感数据就会被放在Referer Header中然后传送过去,然后就呵呵了。另外,有时候因为大家都懂的原因我们会使用一些代理然后允许他们监控HTTPS的流量,也是有危险地,这个时候就要在Header中来关闭缓存从而降低风险。笔者建议你可以参考OWASP Transport Protection Layer Cheat Sheet 来收获一些有用的建议。

Verify Your Configuration

最后一步,你要仔细验证你的配置是否有效。有很多的在线工具可以帮你做这件事,譬如SSL Lab的SSL Server Test能够帮你深度分析你的HTTPS的配置,再看看是不是有啥地方配错了。这个工具会在发现了新的攻击手段与协议更新之后实时更新,所以多用用它还是个很不错的事情嗷。

In Summary

啥地方都要用HTTPS

采用HSTS来强制使用HTTPS

别忘了从可信的证书机构中请求可信证书

不要乱放你的私钥

用合理的配置工具来生成可靠地HTTPS配置

在Cookie中设置"secure"标识

不要把敏感的数据放在URL中

隔一段时间就要好好看看你的HTTPS的配置,表过时了

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/11163.html

相关文章

  • PHPer书单

    摘要:想提升自己,还得多看书多看书多看书下面是我收集到的一些程序员应该看得书单及在线教程,自己也没有全部看完。共勉吧当然,如果你有好的书想分享给大家的或者觉得书单不合理,可以去通过进行提交。讲师温铭,软件基金会主席,最佳实践作者。 想提升自己,还得多看书!多看书!多看书!下面是我收集到的一些PHP程序员应该看得书单及在线教程,自己也没有全部看完。共勉吧!当然,如果你有好的书想分享给大家的或者...

    jimhs 评论0 收藏0
  • 】《精通使用AngularJS开发Web App》(二) --- 框架概览,双向数据绑定,MVC

    摘要:本书的这一部分将为随后的章节打下基础,会涵盖模板,模块化,和依赖注入。本书的小例子中我们会使用未经压缩的,开发友好的版本,在的上。作用域也可以针对特定的视图来扩展数据和特定的功能。 上一篇:【译】《精通使用AngularJS开发Web App》(一) 下一篇:【译】《精通使用AngularJS开发Web App》(三) 原版书名:Mastering Web Application D...

    geekidentity 评论0 收藏0
  • 2017-07-06 前端日报

    摘要:前端日报精选专题之类型判断下百度生态构建发布基于的解决方案将全面支持从绑定,看语言发展和框架设计掘金译机器学习与一付费问答上线,向你心目中的大牛提问吧产品技术日志中文第期团队技术信息流建设翻译基于路由的异步组件加载个必备的装逼 2017-07-06 前端日报 精选 JavaScript专题之类型判断(下) · Issue #30 · mqyqingfeng/Blog 百度Web生态构...

    shiguibiao 评论0 收藏0
  • 微服务简介

    摘要:微服务简介微服务架构是一种架构概念,旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦。每个微服务仅关注于完成一件任务并很好地完成该任务。服务异常自动隔离。微服务架构挑战服务规模大,部署运维管理难度大。 微服务简介 微服务架构(Microservice Architecture)是一种架构概念,旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦。 微服务是一种架构风格,...

    darcrand 评论0 收藏0
  • 网络与安全

    摘要:面试网络了解及网络基础对端传输详解与攻防实战本文从属于笔者的信息安全实战中渗透测试实战系列文章。建议先阅读下的网络安全基础。然而,该攻击方式并不为大家所熟知,很多网站都有的安全漏洞。 面试 -- 网络 HTTP 现在面试门槛越来越高,很多开发者对于网络知识这块了解的不是很多,遇到这些面试题会手足无措。本篇文章知识主要集中在 HTTP 这块。文中知识来自 《图解 HTTP》与维基百科,若...

    Integ 评论0 收藏0

发表评论

0条评论

yanest

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<