关于 XSS 防范的一些思考

标签:HTML, JavaScript

最近在看一些 web 安全相关的文章,大部分都有系统和完善的解决方案,然而 XSS(Cross-site scripting)攻击相关的资料却很杂乱,甚至连 HTML 实体转义能解决哪些地方的 XSS 攻击都说不清。
于是在翻了一堆资料后,觉得还是把自己对它的一些思考记录下来吧。

先要说明的是,不同的地方,有不同的方式避免 XSS:
  • HTML 标签的文本部分,例如:
    <div>{user_input}</div>
    如果 user_data 里包含了 HTML 标签,那么展示的外观(可以加个 img 标签)和逻辑(可以加个 script 标签)都可能被篡改。所以这里至少要把 < 字符转义成 &lt;,也就不会开启和关闭任何标签了。
    单独的 > 字符并不会关闭标签,所以它可以不处理。但是输出成 XML(如 RSS)或 XHTML 格式时,单独的 > 会造成页面解析失败,所以这种情况下需要转义。
    此外,用户在输入 &amp; 的时候,是希望它显示成 &amp; 的,而结果却显示成了 &,这也是不符合预期的。所以 & 字符也应该被转义成 &amp;
    另一个小问题是连续的空格和回车可能都被当成了一个空格,这可以用 white-space: pre-wrap 的样式来处理,也可以替换成 &nbsp;<br>,或者直接不管。
    综上,这里的解决方案是至少转义 <& 字符:
    def escape_html_text(string):
    	return string.replace('&', '&amp;').replace('<', '&lt;')
    function escape_html_text(string) {
    	return string.replace('&', '&amp;').replace('<', '&lt;');
    }
  • HTML 标签的属性部分,例如:
    <input value="{user_input}">
    这里自然也不能允许用户输入 <& 字符。
    此外,如果能输入 > 的话,就可以轻松地关闭 input 标签了,所以 > 需要被转义成 &gt;
    而如果允许输入引号的话,这个属性就能被关闭,然后用户可以接着插入其他属性,所以 "' 需要被转义成 &quot;&#x27;&apos; 是 XML 里的 entity,在 HTML 里没有定义,所以更推荐用前者)。
    此外,IE 允许用 ` 字符作为属性的分割符:
    <input value=`{user_input}`>
    所以有需要的话,也可以转义 ` 字符。但如果你能保证你的的代码里只使用 "' 作为属性分割符,那就可以不处理。
    另一个更重要的事是属性一定要用引号包围起来,虽然写成下面的格式也是可以的,但很难保证不被 XSS 攻击:
    <input value={user_input}>
    如果非要这么做的话,OWASP 建议将属性值中所有的 ASCII 字符都编码成 &#xHH; 的形式。
    回到我们的解决方案,考虑到需要替换的次数太多,所以改用正则表达式效率更高些:
    import re
    
    escape_pattern = re.compile(r'[&<>"\']')
    escape_map = {
    	'&': '&amp;',
    	'<': '&lt;',
    	'>': '&gt;',
    	'"': '&quot;',
    	"'": '&apos;'
    }
    
    def replacer(match):
        return escape_map[match.group(0)]
    
    def escape_html_attr(string):
        return escape_pattern.sub(replacer, string)
    var escape_map = {
    	'&': '&amp;',
    	'<': '&lt;',
    	'>': '&gt;',
    	'"': '&quot;',
    	"'": '&#x27;'
    };
    function replacer(char) {
    	return escape_map[char];
    }
    function escape_html_attr(string) {
    	return string.replace(/[&<>"']/g, replacer);
    }
    其实,Python 也可以直接用 cgi.escape 函数,至少我懒得写这些代码。
    另外,这种替换也适用于 HTML 标签的文本部分,只是输出的字节数可能会变多而已,没别的副作用。
  • HTML 标签的网址属性部分,例如:
    <img src="{user_input}">
    <a href="{user_input}">{user_input}</a>
    <button onclick="{user_input}">
    <link ref="{user_input}" href="{user_input}">
    <script src="{user_input}">
    它们的处理规则其实并不一样:
    • 图片的网址需要对 HTML 属性做编码。
    • 链接的地址需要判断是否是合法的网址(最好是用户输入时就提示),否则用户可以输入 javascript:alert(0) 之类的代码。
    • 后面几个应该不允许用户输入。
  • URL 的参数部分,例如:
    <a href="/path?key={user_input}">...</a>
    需要对参数进行百分号编码(percent-encoding),JavaScript 有内置的 encodeURIComponent 函数(会忽略字母、数字和 - _ . ! ~ * ' ( ) 这些字符),Python 可以用 urllib.quote(url, '-_.!~*()')(我觉得 ' 字符还是比较危险的,不应该忽略)。
  • script 标签的文本部分:
    <script>
    var value = {user_input};
    var value = "{user_input}";
    </script>
    第一种情况应该不允许用户输入。
    第二种情况要考虑 value 之后怎么使用(比如有没有用来 eval,有没有用来生成 HTML 等)。即使后续使用没有在任何有危险的地方,它的处理也很麻烦。OWASP 的建议是把所有 ASCII 字符,除字母和数字以外,都转成 \xHH 的形式。
    代码我就懒得写了,避免出现这种情况才是正道。有兴趣的可以看 ESAPI 的实现,有很多种语言的版本。
  • style 标签的文本部分:
    <style>
    {user_input}
    body {background-url: "{user_input}"}
    </style>
    都不应该允许用户输入。某些允许用户自定义样式的网站,如果会对别人有效,应该提供模板和可选的值,因为这里面能 XSS 攻击的地方太多了。
以上的 XSS 大都还只涉及一种环境,而浏览器不但能解析 HTML,还能解析 JavaScript 和 CSS,同时还得保证 URL 的正确性。所以有时候既要进行 HTML entity 编码,又要进行 JavaScript Hex 编码,还得考虑百分号编码。

此外,还有一些比较省事的解决方案,例如使用久经考验的模板引擎。但建议在已经熟悉 XSS 解决方案的基础上再使用,否则很可能即使用了也还是有 XSS 漏洞。
很多 XSS 风险其实也是来自图省事的心态。如果创建 HTML 元素时,用 document.createElement 方法,然后手动设置各个属性和子元素,自然会安全很多;但拼成字符串,然后直接传给 jQuery()innerHTML 无疑会方便很多,可是风险也就随之突显出来了。

最后,本文还只是粗浅地介绍了一些常见的解决方案,而 XSS 的种类却远非这么几种。
所有输出了不可信数据的地方,都有被 XSS 攻击的可能性。多思考一下用户的输入是否能达到意想不到的结果,并且随着时间的推移,长期地去检视这些可能的隐患点,因为未来很可能出现之前没有发现的攻击方式(例如浏览器的 bug 和没有正确地配置 web 服务器等)。

2条评论 你不来一发么↓ 顺序排列 倒序排列

    向下滚动可载入更多评论,或者点这里禁止自动加载

    想说点什么呢?