3.2 网站攻击和内容安全
要讨论在线服务中的常见问题,OWASP每三年发布的十大Web漏洞是一个不错的出发点。OWASP列出的十大漏洞不仅适用于Web应用自身,还涉及托管这些应用的基础设施的配置错误(第4章将会介绍这些内容),以及敏感组件的升级滞后,这些敏感组件可能会因此完全暴露给已知问题或糟糕的身份验证(对这两个问题,本章后面将做介绍)。在本节中,我们将重点介绍影响Web应用的内容和流程的各种攻击手段,并展示Web浏览器如何防范这些攻击。我们从最常见的跨站脚本攻击说起。
3.2.1 跨站脚本攻击和内容安全策略
在编写本书期间,最常见的网络漏洞恐怕就是跨站脚本攻击了,它通常被叫作XSS(Cross-Site Scripting)。ZAP基线扫描展示了下面两个故障,它们表明发票应用缺少对XSS攻击的防范。
•FAIL:Web浏览器XSS保护未开启(Web Browser XSS Protection Not Enabled)
•FAIL:内容安全策略标头未设置(Content Security Policy Header Not Set)
XSS攻击是由植入到网站中的欺诈代码所导致的,这些代码在植入后,对网站访问者来说就好像是正常的内容。这些欺诈代码会在攻击目标的浏览器中执行并产生危害,例如盗取信息或是冒充用户执行操作。
随着Web应用复杂性的增加,XSS攻击的重要性也在增加,它已经变成了报告最多的现代网站安全问题。我们既然知道发票应用完全暴露在XSS攻击之下,那么我们就先来探讨这个漏洞,然后讨论如何防范它。
你可能想起了在第2章中,发票应用暴露了一些管理发票的终端节点,其中一个会基于在POST请求的正文中提交的JSON数据来创建新发票。把代码清单3.3中的JSON文档当成一次攻击的输入,特别留意其中的description字段。这个字段包含的并不是普通的字符串,而是植入了一段调用JavaScript alert()函数的HTML代码。
将这份文档保存成文件并通过POST发给发票应用API。
当你尝试用浏览器访问API的/invoice/ 终端节点来获取该发票时,如图3.5所示,返回的description字段和你发送的是一模一样的:就是一个字符串,并不会出现什么恶意行为。
图3.5 在浏览器中呈现欺诈的JSON数据不会执行任何攻击。
但是如果你是通过刚刚添加到发票应用的Web界面来访问发票的话,description字段则会作为HTML代码呈现给用户,而不是原始的JOSN。浏览器将〈script〉片段当作代码来解释,并作为页面渲染的一部分来执行。渲染的结果就是包含在恶意载荷中的alert()函数会被触发并弹出警告框,如图3.6所示。
图3.6 在HTML文档中渲染欺诈的JSON将触发对〈script〉代码块的解释,并执行XSS攻击。
为什么在访问原始JSON的时候恶意代码不会被执行?这是因为返回原始JSON的API终端节点还同时返回了一个名为Content-Type的HTTP标头,它的值为application/json。浏览器知道数据不是HTML文档,也就不会执行它的内容。XSS是只存在于HTML页面中的一个问题,页面中的脚本和样式可能会被滥用来执行恶意代码。这种攻击很少出现在Web API上,除非这些API被滥用,把HTML或者订阅源数据返回到了其他HTML页面之中。
XSS攻击有很多不同的形式。刚刚使用的这种攻击尤其危险,因为它将数据永久地保存在发票应用的数据库之中,所以它又被称为持久性XSS。其他类型的XSS不需要在应用数据库中存储数据,而是滥用查询参数的解释。发票应用也容易受到这种类型的XSS攻击,它被称为DOM(Document Object Model)XSS攻击,因为它会修改浏览器的文档对象模型。要执行这种攻击,你需要在一个查询参数(例如invoiceid参数)字符串中植入代码。
在浏览器中输入代码清单3.5中的URL,Web界面就会使用保存在invoiceid参数中的值来渲染部分页面。JavaScript欺诈代码就会被添加到页面的HTML中并被执行。这种类型的XSS要求攻击者将欺诈链接发给攻击目标并让他们点击,这看起来是实施的障碍,但实际上,攻击者可以轻易通过将这些链接隐藏在钓鱼邮件或网页按钮中来达到目的。
那么,应该如何防范XSS攻击呢?有许多可行的方法。对Web应用来说,一般建议如下:
•在提交时对输入进行校验,例如,依次检查收到的发票的每一个字段,使用正则表达式对它们进行检查。
•在渲染之前对所有返回给用户的数据进行转义。大多数语言都有可以转义内容的库。
代码清单3.6展示了在Go语言中内容是如何使用html包来转义的。转义之后的字符串不会被浏览器解释为合法的HTML,因此不会导致代码的执行。
对用户提交的数据进行校验和转义是很有效的技巧。在开发人员的安全工具包中,其应该是保护Web应用的首选工具,但是也有一些缺点:
•开发者要在代码中手动对所有输入输出进行转义,并且确保无一遗漏。
•如果Web应用接收像XML或SVG这样的复杂格式作为输入,那么这些文件中对字段的检验和转义就会破坏文件的结构。
除了通过校验输入和编码转义输出,现代Web应用还应该利用网络浏览器内置的安全特性,其中最有效的可能就是内容安全策略(CSP,Content Security Policy)。
CSP开辟了一条信道,Web应用借助这条信道告知浏览器在渲染网站时要执行什么和不要执行什么。例如,发票应用通过声明一条禁止执行内联脚本的策略,就可以借助CSP来阻止XSS攻击。应用只要在返回的每一个HTTP响应中都带上HTTP标头,就可以声明CSP。
什么是内联脚本?
可以通过两种方式将独立的JavaScript代码嵌入到HTML页面中。代码可以保存在单独文件里,通过标签 〈script src="..."〉 引用这个文件,这个标签将会获取src指定位置的外部资源。或者,代码可以直接加在脚本锚点之间:〈scripts〉alert('test');〈/script〉。第二种方法就是内联代码,因为代码被直接加到了页面里,而不是作为外部资源加载。
代码清单3.7中的策略告诉浏览器开启CSP,它默认会阻止内联脚本,并且只信任来自同一来源(托管发票应用的域)的内容。
利用代码清单3.8中的Go代码,你可以将这个标头设置为与发票应用主页上的每次请求一起返回。
你可以从Web应用路径上的任意基础设施组件上发送CSP标头,比如位于发票应用前面的Web服务器。
尽管让Web服务器返回安全标头是一种不错的方法,可以保证标头每次都能被设置,但我还是建议在应用代码中直接管理CSP,这样开发人员实现和测试起来会更加容易。CI中的ZAP基线扫描将找出缺少CSP标头的页面。
我们再来访问一下启用了CSP的欺诈URL,并在Firefox开发者控制台中检查结果。用右键单击页面,选择Inspect Element(查看元素),然后你就可以进入开发者控制台了。在浏览器底部打开的面板中,单击Console(控制台)标签页,可以查看浏览器在解析页面时返回的错误消息。
在页面的搜索框中输入触发XSS的恶意代码。在没有启用CSP时,它会触发alert('xss')这段代码。而当CSP被启用后,浏览器会拒绝渲染输入,并将以下错误记录在控制台中。
Firefox的UI不会显示任何消息,不会告诉用户攻击已被阻止。禁止的行为被拦截了,而页面的其余部分会正常渲染,就好像什么都没发生一样。仅有的违规迹象出现在开发者控制台中,如图3.7所示。
图3.7 CSP告知浏览器不要执行内联脚本,这样就拦截了XSS攻击。
CSP通过阻止欺诈脚本在浏览器中执行来保护应用的用户。这种方法的优势是,用一条简单策略就可以防止大范围的攻击。然而,这个例子被极大地简化了,现代Web应用通常需要复杂的CSP指令来让各种组件协作。代码清单3.10展示了链接3.7中的CSP,它使用了比发票应用复杂得多的策略。
拯救老网站的CSP
我不是随随便便地选中了Mozilla的插件网站。它是Mozilla年代最久远的网站之一,同时它还是风险级别最高的网站,因为它管理着Firefox使用的插件。在几年前,它的老代码还特别容易受到XSS攻击,我们几乎每周都会收到来自问题赏金计划的漏洞报告,直到我们启用了CSP!在一天之内,报告就消失不见了,工程师们再也不用陪漏洞玩打地鼠的游戏了,取而代之的是他们对网站改进地全身心投入。
这里我将略过代码清单3.10中的策略细节。如果你对这种复杂的机制感兴趣,请查阅MDN上的文档(链接3.8)。CSP很复杂,而且可能还很难实现。现代Web应用是动态的,而且会以不同的方式和网络浏览器及第三方进行交互。CSP提供了一种方法来定义不能被接受的交互。这可以大大提升安全性,但也需要一些投入。CSP的复杂性是它应该由应用开发人员来直接管理的原因,不要完全指望由安全团队来解决它。
我们回到TDS模型,看看发票应用在启用CSP之后的ZAP基线扫描。
现在,在测试结果中找不到关于XSS及CSP的两条故障了,这就是代码清单3.8中的补丁在提交后所期望的结果。这个补丁在发票应用的主页中添加了CSP标头。现在我们可以专注于清单中的另一条故障——跨站请求伪造(CSRF,Cross-Site Request Forgery)。
3.2.2 跨站请求伪造
一个网站可以链接位于另一个网站里的资源,这是Web的核心概念。如果网站之间的协作都以互相尊重为基础,不去尝试使用超链接来修改对方的内容,这种模型的效果就很好,但它却不能杜绝滥用。CSRF攻击就是这样做的:滥用网站间的链接,强迫用户执行本不打算执行的操作。
来看看图3.8中展示的流程。用户不知何故被骗去访问恶意网站,可能是由于钓鱼邮件或者其他什么方式。在第一步连接到恶意网站的时候,返回给浏览器的HTML中包含了一个指向链接3.9的图片链接。在第二步中,浏览器在处理由HTML构建的页面时,向该图片的URL发送了GET请求。
这个URL中并没有任何图片,因为GET请求的目的是删除一份发票。发票应用对正在进行的攻击一无所知,它认为这个请求是合法的,并删除了数据库中编号为2的发票。恶意网站成功地迫使用户伪造了一个跨越到发票应用的请求,这种攻击因此而得名:跨站请求伪造。
你可能会想:“不应该是发票应用网站的身份验证来拦截这种攻击吗?”这在某种程度上是对的,但仅限于在攻击发生时用户还没有登录到发票应用的这种情况。如果用户已经登录到了发票应用,并且正确的会话cookie已经被保存到了本地,那么浏览器会把这些会话cookie和GET请求一起发出去。在发票应用看来,删除操作是完全合法的。
图3.8 CSRF攻击诱骗用户访问恶意网站(1),并且未经他们同意就向发票应用网站发送请求(2)。
我们可以使用跟踪令牌来防范CSRF,这个令牌在构建主页时会发给用户,然后在提交删除请求时由浏览器发送回来。因为恶意网站的操作是盲目的,无法访问发票应用和浏览器之间交换的数据,所以它不能强迫浏览器在触发欺骗性的删除请求时发送令牌。发票应用只需要在采取行动前确认令牌是存在的即可。如果令牌不存在,请求就是非法的,应该被拒绝。
在发票应用中实现CSRF令牌的技术有很多种方式。我们选择的这种HMAC加密算法不需要在服务器端维护状态。HMAC,即哈希密钥验证码(Hash-based Message Authentication Code),是一种哈希算法,它根据输入值和密钥来生成一个固定长度的输出值(无论输入的长度是多少)。你可以使用HMAC生成一个唯一的令牌并提供给网站访问者,后续的请求将使用这个令牌进行验证以防止CSRF攻击。
发票应用在每次主页被请求的时候将生成独一无二的HMAC,其结果就是CSRF令牌。当浏览器向发票应用发起删除请求的时候,HMAC会被校验,只有校验通过,请求才会被处理。图3.9展示了CSRF签发和验证的流程。
图3.9 当用户访问发票应用的主页时,发票应用给用户签发一个CSRF令牌(最上面的GET/请求)。接下来的POST/invoice请求必须一并提交这个CSRF令牌,以确保用户在发起其他请求之前先访问主页,而不是被胁迫要通过第三方网站来发送POST请求。
当用户访问发票应用的主页时,返回浏览器的HTML文档包含了一个独一无二的CSRF令牌,它的名字是CSRFToken,它保存在表单数据的一个隐藏字段里。代码清单3.13是从HTML页面中提取出的一部分内容,并展示了表单隐藏字段里的CSRF令牌。
在提交表单时,同样是来自主页的JavaScript代码将从表单的值里获取令牌,并放到请求的HTTP标头X-CSRF-Token里,这个请求将被发送给发票应用。代码清单3.14中的代码使用jQuery框架发送带令牌的请求。你可以查看发票应用源代码仓库中的statics/invoicer-cli.js,在该文件的getInvoice()函数里可以找到相关代码。
在发票应用这边,处理发票删除的终端节点在处理请求之前,先从HTTP标头里获取令牌并调用checkCSRFToken(),以及对HMAC进行验证。这段代码如代码清单3.15所示。
发票应用通过生成另一个新的令牌来验证提交过来的令牌,这个新的令牌使用接收到的用户数据和只有发票应用可以访问的密钥生成。[1]如果两个令牌相等,发票应用就可以信任收到的用户请求。如果验证失败,那么请求不会被处理,还会返回给浏览器一个错误代码。要破解这个方案,必须破解HMAC背后的加密算法(SHA256)或者取得密钥,这都是极难实现的。
回到攻击的例子,这一次我们启用了CSRF令牌。攻击者在恶意网站上设置的代码〈img src〉仍然会生成一个发给发票应用的请求,但是不会包含正确的CSRF令牌。发票应用会以错误代码406 Not Acceptable拒绝该请求,如图3.10中的Firefox开发者控制台所示。
应用和浏览器之间的令牌舞蹈可能很快就会变得复杂起来,在一个大型应用上实现CSRF不是一个轻松的任务。因此,许多Web框架自动支持CSRF令牌。开发者几乎不需要自己来实现令牌,但深入了解这种攻击方式及其防范方法可以帮助你指导DevOps团队如何来保护Web应用。
图3.10 被诱骗进行CSRF攻击的用户因为缺少令牌而得到了保护;他们的请求没有被发票应用处理。
SameSite cookie
在编写本书时,网络浏览器开始集成一个新的参数:SameSite cookie,它能更简单地规避CSRF攻击。如果应用开发者在给定的cookie上设置了属性SameSite=strict,那么就是告知浏览器用户:只有在直接浏览目标网站(浏览器地址栏里就是这个网站)的时候才会发送该cookie。比如,发票应用网站设置的带SameSite属性的cookie不会与在访问恶意网站时发起的请求一起发送,因此可以阻止恶意网站向发票应用网站发起CSRF攻击。
在将来,SameSite属性可能会成为会话cookie的标准,这样就可以完全避免CSRF攻击。但是老浏览器缺少SameSite支持的长尾效应,这意味着应用还要保持向后兼容性,以便能处理不能访问该属性的情况,而且应该优先选择基于HMAC的CSRF令牌。
实现了CSRF令牌之后,再次运行基线扫描,以确认缺少防CSRF令牌的故障是否已经消失。
只剩一个了!下一个关注的主题是解决X-Frame-Options问题和消除点击劫持攻击的影响。
3.2.3 点击劫持和IFrame保护
在互联网诞生的早期,网站常常使用内联窗口和HTML标签 〈iframe〉来互相内嵌对方的内容。如今,这种方法已经不受欢迎,网站倾向于使用更优雅的技术将不同的来源组装成网站。然而,IFrame技术仍然存在,还被浏览器全面支持,这可能会导致一种非常危险的攻击向量——点击劫持。
欺诈网站利用点击劫持技术欺骗用户点击一个指向另一个网站的隐藏链接。我们来看这个例子:恶意网站创建了一个指向发票应用网站的IFrame,页面上只有这个IFrame使用样式指令来渲染成对用户不可见。用户被诱骗访问恶意网站并点击一个链接,用户不知道的是这个链接实际上是发票应用网站上的一个按钮,这个按钮在屏幕上是看不见的。
图3.11展示了对发票应用网站主页的点击劫持攻击。图中左侧,透明度被设置成了50%,可以看到恶意网站的CLICK ME! 链接正好被发票应用网站主页上的Delete This Invoice按钮盖住了。图中右侧,使用CSS指令opacity:0就让发票应用网站的IFrame完全看不见了。用户以为他们点击的是CLICK ME! 按钮,而实际上,覆盖在上面的发票应用网站IFrame让他们点击了Delete This Invoice按钮。
图3.11 点击劫持攻击使用了隐藏的IFrame(图中左边将它的透明度设置成了50%),欺骗用户点击目标网站的链接。
和CSRF类似,浏览器在处理欺诈请求时会使用现有的身份验证和会话。从浏览器和发票应用的角度看,欺诈点击是合法的。
浏览器早就意识到了点击劫持的风险,并实现了有针对性的防护措施。这些防护措施默认是没有启用的,需要手动添加。防止点击劫持的现代方法是使用CSP来为child-src'self'设置策略,这表示网站只允许同一来源的页面嵌入IFrame之中。
正如ZAP基线扫描提示的那样,另一种防止点击劫持的方法是设置HTTP标头X-FRAME-OPTIONS。返回值为SAMEORIGIN的这个标头,和使用CSP指令阻止浏览器加载恶意网站中的invoicer IFrame的效果是一样的。不是所有的浏览器现在都支持CSP的child-src指令,所以使用X-FRAME-OPTIONS再加上CSP才是保护所有浏览器的有效方法。
由于你已经在发票应用的主页上设置了CSP,你可以在其中加入child-src来扩展它,还要加上X-FRAME-OPTIONS标头。代码清单3.17扩展了已经在代码清单3.8中设置过的标头。
搞定了最后这一个问题,基线扫描将成功通过,返回的退出码是0,并且允许CircleCI将构建过程继续进行下去。将来如果再次引入任何漏洞,自动化扫描都会捕获它并立即报告给开发者。
基线扫描覆盖了许多问题,但其中有一些是和应用的业务逻辑相关的,需要用不同的方式处理。你可能已经注意到了发票应用现在还没有任何身份验证,对一个被设计用来管理敏感数据的应用来说,这是很令人担忧的。ZAP无法提醒你这一点,因为它不知道哪些资源需要身份验证。在下一节,我们将讨论网站和Web API对用户进行身份验证的常见技术。