Web安全之XSS的防御

4次阅读

共计 4122 个字符,预计需要花费 11 分钟才能阅读完成。

HttpOnly

浏览器禁止页面的 JavaScript 访问带有 HttpOnly 属性的 Coockie。它解决的是 XSS 后的 Cookie 劫持攻击。
HttpOnly 是在服务器返回的响应头 Set-Cookie 上标记的:

Set-Cookie: <name>=<value>[; <Max-Age>=<age>]
[; expires=<date>][; domain=<domain_name>]
[; path=<some_path>][; secure][; HttpOnly]

输入检查

XSS 要求攻击者构造一些特殊字符,这些特殊字符可能是正常用户不会用到的,所以输入检查就有存在的必要了。

输入检查的逻辑,必须放在服务端代码中实现。如果只是在客户端使用 JavaScript 进行输入检查,很容易被攻击者绕过。目前的普遍做法是,同时在客户端和服务端实现相同的输入检查。客户端检查可以阻挡大部分误操作的用户,从而节省服务器资源。

XSS 输入检查一般会检查用户输入的数据中是否包含一些特殊字符,如 <、>、’、“等,将这些字符过滤或者编码。

比较智能的输入检查,可能会匹配 XSS 的特征。比如查找用户数据中是否包含了<script>、javascript 等敏感字符。

这种输入检查的方式,称为 XSS Filter。

XSS Filter 只获取了用户提交的数据进行检查,但是并没有结合渲染页面的 HTML 代码,因此对语境的理解并不完整。

例如:

<script src="$var"></script>

用户只需要提交一个恶意脚本所在的 URL 地址,即可实施 XSS 攻击。而大多数情况下,URL 是合法的用户数据。

XSS Filter 还有一个问题,对 <、> 的处理可能会改变用户语义。
比如

1+1<3

如果 XSS Filter 不够智能,粗暴地过滤或者替换 <,则会改变用户原来的意思。

输出检查

既然输入检查存在这么多问题,那输出检查又如何呢
一般来说,除了富文本输出外,在变量输出到 HTML 页面时,可以使用编码或转义的方式来防御 XSS 攻击。

安全的编码函数

编码分为很多种,针对 HTML 代码的编码方式是 HtmlEncode。
HtmlEncode 并非专有名词,它只是一种函数实现。它的作用是将字符转换成 HTMLEntities, 对应的标准是 ISO-8859-1

为了对抗 XSS, 在 HTMLEncode 中要求至少转换以下字符:
& -> &amp;
< -> &lt;
> -> &gt;
” -> &quot;
‘ -> &#x27;
/ -> &#x2F

Javascript 的编码方式可以使用 JavaScriptEncode
它使用 ”” 对特殊字符进行转义。在对抗 XSS 时还要求输出的变量必须在引号内部,以避免造成安全问题。
比较两种写法

var x = escapeJavaScript($evil);
var y = '"'+escapeJavaScript($evil)+'"';

如果只是转义了几个危险字符,如’、”、<、>、、&、# 等,那么可能会输出

var x = 1;alert(2);
var y = "1;alert(2);";

攻击者即使想要逃脱出引号的范围,也会遇到困难。

var y = "\";alert(1);\/\/";

但是开发者没有这个习惯怎么办?
那就只能使用一个更加严格的 JavaScriptEncode 函数来保证安全了——除了数字、字母外的所有字符,都使用十六进制“xHH”的方式进行编码

var x = 1;alert(2);

变成了

var x = 1\x3balert\x282\x29;

此外还有其他编码函数,如 XMLEncode、JSONEncode 等

只需要一种编码吗

XSS 主要发生在 MVC 架构中的 View 层。大部分的 XSS 漏洞可以在模板系统中解决。

如 Python 的开发框架 Django 自带的模板系统 ”Django Templates”,可以使用 escape 进行 HtmlEncode, 并且在 Django1.0 中得到了加强——默认所有的变量都会被 escape。

但这样还是不能完全避免 XSS 问题,需要“在正确的地方使用正确的编码方式”
例如

<body>
  <a href=# onclick="alert('$var');" >test</a>
</body>

如果用户输入

$var = htmlencode("');alert('2");

对于浏览去器来说,htmlparser 会优先于 JavaScript Parser 执行
经过解析后,结果为

<body>
  <a href=# onclick="alert('');alert('2');" >test</a>
</body>

xss 被注入。

xss 发出的原因是,没有分清楚输出变量的语境,并非在模板引擎中使用了 auto-escape 就万事大吉了。

正确防御 XSS

XSS 的本质是一种“HTML 注入”,想要根治 XSS 问题,可以列出所有 XSS 可能发生的场景,再一一解决。

在 HTML 标签中输出

<div>$var</div>
<a href=# >$var</a>

防御的方法是对变量使用 HtmlEncode

在 HTML 属性中输出

<div id="abc" name="$var" ></div>

防御的方法是对变量也是使用 HtmlEncode

在 script 标签中输出

首先应该确保输出的变量在引号中:

<script>
  var x = "$var";
</script>

防御时使用 JavaScriptEncode

在事件中输出

<a href=# onclick="funcA('$var')" >test</a>

防御方式和在 script 标签中输出类似

在 CSS 中输出

比如 @import、url 等直接加载 XSS、expression(alert(‘xss’))等
一方面需尽可能避免用户可控制的变量在 css 中输出,如果必须有这样的需求,那么使用 encodeForCSS

在地址栏输出

一般来说使用 URLEncode 即可,但是如果整个 URL 被用户完全控制,Protocal 和 Host 是不能使用 URLEncode 的,否则会改变 URL 的语义。
[Protocal][Host][Path][Search][Hash]
例如 https://www.evil.com/a/b/c/te…
对于如下的输出方式:

<a href="$var" >test</a>

攻击者可能会构造伪协议实施攻击

<a href="javascript:alert(1);" >test</a>

此外,“vbscript”、“dataURI”等伪协议也可以导致脚本执行。

一般来说,对于用户控制整个 URL,应该先检查是否以“http”开头,如果不是则自动添加,以保证不会出现伪协议类的 XSS 攻击,然后再对变量进行 URLEncode。

处理富文本

网站允许用户提交一些自定义的 HTML 代码,称之为富文本。

在处理富文本时,还是要回到“输入检查”的思路上来。“输入检查”的主要问题是检查时还不知道变量的输出语境。但富文本数据,其语义是完整的 HTML, 在输出时也不会拼凑到某个标签的属性中。因此,可以特殊对待。

通过 htmlparser 可以解析出 HTML 代码的标签、标签属性和事件。
在过滤富文本时,事件应该被严格禁止、而一些危险的标签,比如 iframe、script、base、form 等也是应该严格禁止的。在标签选择上,应该使用白名单。比如,只允许 a、img、div 等比较安全的标签。“白名单”同样应该用于属性和事件的选择。

在富文本过滤中,处理 CSS 也是一件麻烦的事,因此应该禁止用户自定义 CSS 和 style。如果不能禁止,那就需要一个 CSS Parser 对样式进行智能分析,检查其中是否包含危险代码。

防御 DOM Based XSS

DOM Based XSS 前文提到的几种防御方法都不太适用。

DOM Based XSS 是从 JavaScript 中输出数据到 HTML 页面里,而前文都是针对从服务器应用直接输出到 HTML 页面的 XSS 漏洞。
服务器执行了 javascriptEscape,输出的数据又重新渲染到了页面中,对变量进行了解码,仍然会产生 XSS。

正确的防御方法是,在 $var 输出到 script 时,执行一次 javaScriptEncode,其次在输出到 HTML 页面时,如果输出到事件或者脚本,则要再做一次 javaScriptEncode;如果输出到 HTML 内容或者属性,则再做一次 HtmlEncode。

触发 DOM Based XSS 的地方有很多,以下几个地方是 JavaScript 输出到 HTML 页面的必经之路。
document.write()
document.writeln()
xxx.innerHTML=
xxx.outerHTML=
innerHTML.replace
document.attachEvent()
window.attachEvent()
document.location.replace()
document.location.assign()

需要重点关注这几个地方的参数是否可以被用户控制
除了服务器端直接输出变量到 JavaScript 外,还有以下几个地方可能成为 DOM Based XSS 的输出点,也需要重点关注。
inputs 框
window.location(href、hash 等)
window.name
document.referrer
document.cookie
localstorage
XMLHttpRequest 返回的数据

换个角度看 XSS 的风险

前面都是从漏洞的形成原理上看的,如果从业务风险角度看则有不同的观点

一般来说,存储型 XSSS 的风险高于反射型。因为它保存在服务器上,有可能会跨页面存在。它不会改变页面 URL 的原有结构,因此有时候还能逃过一些 IDS 的检测。

从攻击过程来说,反射型 XSS,一般要求攻击者诱使用户点击一个包含 XSS 代码的 URL 链接;而存储型 XSS, 则只需要让用户查看一个正常的 URL 链接。

从风险角度看,用户之间有互动的页面,是可能发起 XSS Worm 攻击的地方。而根据不同页面的 PageView 高低,也可以分析出哪些页面受 XSS 攻击后的影响更大。

在修补漏洞时遇到的最大挑战之一是漏洞数量太多,开发者不太来得及立刻修复所有漏洞。从业务风险角度来重新定位每个 XSS 漏洞,就具有了重要意义。

正文完
 0