前言

Web安全一直是老生常谈的一个问题,下面就讲解关于XSS的内容
关键词:XSS的由来、XSS类型、XSS预防手段、XSS检测。

XSS是什么?

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击,攻击者通过在目标网站上面注入恶意的脚本,让其在用户的浏览器上面运行。利用这些脚本就能轻易获取到用户的cookie,sessionID等,产生进一步安全威胁。

在命名上为了和css区分,改为了xss

在处理输入的时候,以下内容不可信:

  • 来自用户的 UGC 信息
  • 来自第三方的链接
  • URL 参数
  • POST 参数
  • Referer (可能来自不可信的来源)
  • Cookie (可能来自其他子域注入)

XSS分类

根据攻击来源,可以分为存储型,反射型和DOM型

存储型

攻击流程:

  1. 攻击者将恶意代码提交到网站的数据库中
  2. 用户响应服务端的恶意代码
  3. 恶意代码窃取用户敏感信息发送到攻击者网站,或冒充用户行为执行操作。

常见场景:
论坛发帖,商品评论,用户私信

反射型

攻击流程:

  1. 攻击者构造出特殊的URL里面包含恶意代码
  2. 用户被诱导点击特殊URL,执行恶意操作。

反射型和存储型的区别在于,一个是存在URL里面,一个是存在数据库里面。

常见场景:
网站搜索,跳转。

需要用户主动去点击,所以攻击者常有多种手段诱导用户点击。

另外注意POST也可以诱发反射型XSS,不过需要构建表单提交界面所以少见。

DOM型

攻击流程:

  1. 攻击者构造出特殊的URL里面包含恶意代码
  2. 用户打开带有恶意代码的URL
  3. 浏览器执行恶意代码

DOM型和前两种的区别在于,DOM型的攻击是由浏览器端完成的,属于js自身的漏洞,其他两种都是服务器端的安全漏洞

XSS攻击预防

上述了解到,XSS攻击有两种要素

  1. 攻击者提交恶意代码
  2. 浏览器执行恶意代码

针对第一个要素,有以下的防范措施

输入过滤

前端过滤—>后端?
不可行 绕过过滤,直接构造请求就可以提交了

数据库输入过滤—>前端?
不一定可行。
eg 输入5<7 在写入数据库前被转义成5 &lt; 7
问题是我们在提交阶段不清楚内容要输出到哪里。也就是意味着我们的内容

  1. 用户提交到前端和客户端,经过了escapeHTML()转义,客户端就乱码了
  2. 前端不同地方需要的编码也不一样
    • 5 &lt; 7 作为 HTML 拼接页面时,可以正常显示:
    • 当作为AJAX返回的时候,这个内容要经过处理才可以显示。

所以在输入侧是可以解决这个问题但是有乱码和不确定性的问题,导致工程量加大。

输入侧不行就要通过浏览器来防范

  1. 防止HTML注入
  2. 防止JS执行的时候执行恶意代码

预防存储型和反射型

存储型和反射型都是在服务端取出恶意代码后插入到HTML里,攻击者将数据内嵌到代码中,被浏览器执行

解决方法

  1. 纯前端渲染,数据和代码隔离
  2. 对HTML做充分转义

纯前端渲染

过程:

  1. 加载静态HTML,没有任何业务数据
  2. 浏览器执行HTML中的js代码
  3. 数据通过AJAX加载

关键在于,我们要告诉浏览器下面要设置的内容是文本(.innerText),还是属性(.setAttribute),还是样式(.style)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。

注意避免DOM型XSS 比如onload事件和herf中的javascript:xxx

在很多内部、管理系统中,采用纯前端渲染是非常合适的。但对于性能要求高,或有 SEO 需求的页面,我们仍然要面对拼接 HTML 的问题。

转义HTML

利用转义库来进行防护

预防DOM型XSS

不要用innerHTMLdocument.write系列代码
textContent,setAttribute系列代码

在vue/react中且不使用v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患。

DOM 中的内联事件监听器,如 locationonclickonerroronloadonmouseover 等,<a> 标签的 href 属性,JavaScript 的 eval()setTimeout()setInterval()
都能进行xss攻击,注意防范字符串拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 内联事件监听器中包含恶意代码 -->
<img onclick="UNTRUSTED" onerror="UNTRUSTED" src="data:image/png,">

<!-- 链接内包含恶意代码 -->
<a href="UNTRUSTED">1</a>

<script>
// setTimeout()/setInterval() 中调用恶意代码
setTimeout("UNTRUSTED")
setInterval("UNTRUSTED")

// location 调用恶意代码
location.href = 'UNTRUSTED'

// eval() 中调用恶意代码
eval("UNTRUSTED")
</script>

其他防范

以下是一些通用方案

Content Security Policy

严格的 CSP 在 XSS 的防范中可以起到以下的作用:

  • 禁止加载外域代码,防止复杂的攻击逻辑。
  • 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
  • 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
  • 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
  • 合理使用上报可以及时发现 XSS,利于尽快修复问题。

关于 CSP 的详情,请关注前端安全系列后续的文章。

输入内容长度控制

对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。

其他安全措施

  • HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
  • 验证码:防止脚本冒充用户提交危险操作。

XSS检测

  1. 使用通用 XSS 攻击字符串手动检测 XSS 漏洞。
  2. 使用扫描工具自动检测 XSS 漏洞。

Unleashing an Ultimate XSS Polyglot一文中,小明发现了这么一个字符串:

1
https://github.com/0xsobky/HackVault/wiki/Unleashing-an-Ultimate-XSS-Polyglot

它能够检测到存在于 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等多种上下文中的 XSS 漏洞,也能检测 eval()、setTimeout()、setInterval()、Function()、innerHTML、document.write() 等 DOM 型 XSS 漏洞,并且能绕过一些 XSS 过滤器。

小明只要在网站的各输入框中提交这个字符串,或者把它拼接到 URL 参数上,就可以进行检测了。

1
http://xxx/search?keyword=jaVasCript%3A%2F*-%2F*%60%2F*%60%2F*%27%2F*%22%2F**%2F(%2F*%20*%2FoNcliCk%3Dalert()%20)%2F%2F%250D%250A%250d%250a%2F%2F%3C%2FstYle%2F%3C%2FtitLe%2F%3C%2FteXtarEa%2F%3C%2FscRipt%2F--!%3E%3CsVg%2F%3CsVg%2FoNloAd%3Dalert()%2F%2F%3E%3E

除了手动检测之外,还可以使用自动扫描工具寻找 XSS 漏洞,例如 Arachni、Mozilla HTTP Observatory、w3af 等。

总结

我们回到最开始提出的问题,相信同学们已经有了答案:

XSS 防范是后端 RD 的责任,后端 RD 应该在所有用户提交数据的接口,对敏感字符进行转义,才能进行下一步操作。

不正确。因为:

防范存储型和反射型 XSS 是后端 RD 的责任。而 DOM 型 XSS 攻击不发生在后端,是前端 RD 的责任。防范 XSS 是需要后端 RD 和前端 RD 共同参与的系统工程。
转义应该在输出 HTML 时进行,而不是在提交用户输入时。

所有要插入到页面上的数据,都要通过一个敏感字符过滤函数的转义,过滤掉通用的敏感字符后,就可以插入到页面中。

不正确。
不同的上下文,如 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等,所需要的转义规则不一致。
业务 RD 需要选取合适的转义库,并针对不同的上下文调用不同的转义规则。

整体的 XSS 防范是非常复杂和繁琐的,我们不仅需要在全部需要转义的位置,对数据进行对应的转义。而且要防止多余和错误的转义,避免正常的用户输入出现乱码。
虽然很难通过技术手段完全避免 XSS,但我们可以总结以下原则减少漏洞的产生:

  • 利用模板引擎
    开启模板引擎自带的 HTML 转义功能。例如:
    在 ejs 中,尽量使用 <%= data %>而不是 <%- data %>
    在 doT.js 中,尽量使用 {{! data }` 而不是` {{= data }`; 在 FreeMarker 中,确保引擎版本高于 2.3.24,并且选择正确的 `freemarker.core.OutputFormat`。 * 避免内联事件 尽量不要使用` onLoad="onload('{{data}}')"onClick="go('{{action}}')" 这种拼接内联事件的写法。在 JavaScript 中通过 .addEventlistener() 事件绑定会更安全。
  • 避免拼接 HTML
    前端采用拼接 HTML 的方法比较危险,如果框架允许,使用 createElement、setAttribute 之类的方法实现。或者采用比较成熟的渲染框架,如 Vue/React 等。
  • 时刻保持警惕
    在插入位置为 DOM 属性、链接等位置时,要打起精神,严加防范。
  • 增加攻击难度,降低攻击后果
    通过 CSP、输入长度配置、接口安全措施等方法,增加攻击的难度,降低攻击的后果。
  • 主动检测和发现
    可使用 XSS 攻击字符串和自动扫描工具寻找潜在的 XSS 漏洞。