乐趣区

Web安全xss防护实践

XSS

XSS (Cross-Site Scripting),跨站脚本攻击,因为缩写和 CSS 重叠,所以只能叫 XSS。跨站脚本攻击是指通过存在安全漏洞的 Web 网站注册用户的浏览器内运行非法的 HTML 标签或 JavaScript 进行的一种攻击。

跨站脚本攻击有可能造成以下影响:

  • 利用虚假输入表单骗取用户个人信息。
  • 利用脚本窃取用户的 Cookie 值,被害者在不知情的情况下,帮助攻击者发送恶意请求。
  • 显示伪造的文章或图片。

XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的

XSS 的攻击方式千变万化,但还是可以大致细分为几种类型。

  • 存储型 XSS:注入型脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器上传回并执行。
  • 反射型 XSS:当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web 服务器将注入脚本,比如一个错误信息,搜索结果等 返回到用户的浏览器上。浏览器会执行这段脚本,因为,它认为这个响应来自可信任的服务器。
  • 基于 DOM 的 XSS:被执行的恶意脚本会修改页面脚本结构

XSS 最终是要以“输出编码”来完成攻击的,我们将“输出编码”的攻击方法总结为以下几种

  • 在 HTML 标签中输出变量;
  • 在 HTML 属性中输出变量;
  • 在 script 标签中输出变量;
  • 在事件中输出变量;
  • 在 CSS 中输出变量;
  • 在 URL 中输出变量。

表现为:

  • 在 HTML 中内嵌的文本中,恶意内容以 script 标签形成注入。
  • 在内联的 JavaScript 中,拼接的数据突破了原本的限制(字符串,变量,方法名等)。
  • 在标签属性中,恶意内容包含引号,从而突破属性值的限制,注入其他属性或者标签。
  • 在标签的 href、src 等属性中,包含 ​javascript:​ 等可执行代码。
  • 在 onload、onerror、onclick 等事件中,注入不受控制代码。
  • 在 style 属性和标签中,包含类似 ​background-image:url(“javascript:…”);​ 的代码(新版本浏览器已经可以防范)。
  • 在 style 属性和标签中,包含类似 ​expression(…)​ 的 CSS 表达式代码(新版本浏览器已经可以防范)。
  • 在 url 参数上(下面有例子)

反射型 XSS

特征:

攻击者可以直接通过 URL (类似:​https://xxx.com/xxx?default=<script>alert(document.cookie)</script></script>)​) 注入可执行的脚本代码。不过一些浏览器如 Chrome 其内置了一些 XSS 过滤器,可以防止大部分反射型 XSS 攻击。

措施

  • Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。
  • 尽量不要从 ​URL​,​document.referrer​,​document.forms​ 等这种 DOM API 中获取数据直接渲染。
  • 尽量不要使用 ​eval​, ​new Function()​,​document.write()​,​document.writeln()​,​window.setInterval()​,​window.setTimeout()​,​innerHTML​,​document.createElement()​ 等可执行字符串的方法。
  • 如果做不到以上几点,也必须对涉及 DOM 渲染的方法传入的字符串参数做 escape 转义。
  • 前端渲染的时候对任何的字段都需要做 escape 转义编码。

_// 防御 xss 漏洞_

_// 对字符串进行 XSS 过滤_

_//_ _https://eggjs.org/zh-cn/core/security.html#%E5%BC%80%E5%90%AF%E4%B8%8E%E5%85%B3%E9%97%AD%E9%85%8D%E7%BD%AE_

**function** decodeInput(char) {

switch (char) {

case "&":

return "&amp;";

case "<":

return "&lt;";

case ">":

return "&gt;";

case '"':

return "&quot;";

case "'":

return "&#39;";

case " ":

return "&nbsp;";

default:

return char + "";

}

}

如果拼接 HTML 是必要的,就需要采用合适的转义库,对 HTML 模板各处插入点进行充分的转义。

常用的模板引擎,如 doT.js、ejs、FreeMarker 等,对于 HTML 转义通常只有一个规则,就是把 ​& < > ” ‘ /​ 这几个字符转义掉,确实能起到一定的 XSS 防护作用,但并不完善:

XSS 安全漏洞 简单转义是否有防护作用
HTML 标签文字内容 例如前端工程,针对 html  使用 https://jsxss.com/zh/index.html
HTML 属性值 例如前端工程,针对 html  使用 https://jsxss.com/zh/index.html
CSS 内联样式 例如前端工程,针对 html  使用 https://jsxss.com/zh/index.html
内联 JavaScript 过滤,包括前端输入时候,服务端入库前,服务端输出前,前端触发前
内联 json 实现 escapeJson 函数
跳转链接 单独处理

所以要完善 XSS 防护措施,我们要使用更完善更细致的转义策略。

例如 Java 工程里,常用的转义库为 ​org.owasp.encoder​。以下代码引用自 org.owasp.encoder 的官方说明

_<!-- HTML 标签内文字内容 -->_

<div><%= Encode.forHtml(UNTRUSTED) %></div>

_<!-- HTML 标签属性值 -->_

<input value="<%= Encode.forHtml(UNTRUSTED) %>" />

_<!-- CSS 属性值 -->_

<div style="width:<= Encode.forCssString(UNTRUSTED) %>">

_<!-- CSS URL -->_

<div style="background:<= Encode.forCssUrl(UNTRUSTED) %>">

_<!-- JavaScript 内联代码块 -->_

<script>

var msg = "<%= Encode.forJavaScript(UNTRUSTED) %>";

alert(msg);

</script>

_<!-- JavaScript 内联代码块内嵌 JSON -->_

<script>

var __INITIAL_STATE__ = JSON.parse('<%= Encoder.forJavaScript(data.to_json) %>');

</script>

_<!-- HTML 标签内联监听器 -->_

<button

onclick="alert('<%= Encode.forJavaScript(UNTRUSTED) %>');">

click me

</button>

_<!-- URL 参数 -->_

<a href="/search?value=<%= Encode.forUriComponent(UNTRUSTED) %>&order=1#top">

_<!-- URL 路径 -->_

<a href="/page/<%= Encode.forUriComponent(UNTRUSTED) %>">

_<!--_

_URL._

_注意:要根据项目情况进行过滤,禁止掉 "javascript:" 链接、非法 scheme 等_

_-->_

<a href='<%=

urlValidator.isValid(UNTRUSTED) ?

Encode.forHtml(UNTRUSTED) :

"/404"

%>'>

link

</a>

场景实践

  1. 进入漏洞 url: xxx.com/login?_redirect=javascript:alert(1)) (预览环境已经回退,可线查看)
  2. 扫码登陆
  3. 成功弹窗

针对这种,防护比较简单,我们直接过滤即可。

这个问题 StackOverflow 有类似讨论 is-it-secure-to-use-window-location-href-directly-without-validation

  • 做了 HTML 转义,并不等于高枕无忧。
  • 对于链接跳转,如 ​<a href=”xxx”​ 或 ​location.href=”xxx”​,要检验其内容,禁止以 ​javascript:​ 开头的链接,和其他非法的 scheme。完善一点解决的话,应该如下。

_// 根据项目情况进行过滤,禁止掉_ _"javascript:"_ _链接、非法 scheme 等_

allowSchemes = ["http", "https"];

valid = isValid(getParameter("redirect_to"), allowSchemes);

if (valid) {

<a href="<%= escapeHTML(getParameter("redirect_to"))%>">

跳转...

</a>

} else {

<a href="/404">

跳转...

</a>

}

存储型 XSS(持久型)

存储型 XSSXSS 漏洞,一般存在于 Form 表单,富文本编辑 提交等交互功能,如文章留言,提交文本信息等,黑客利用的 XSS 漏洞,将内容经正常功能提交进入数据库持久保存,当前端页面获得后端从数据库中读出的注入代码时,恰好将其渲染执行。

场景实践

<SCRIPT SRC=http://xss.rocks/xss.js></SCRIPT>

去掉 script 即可,因为无论怎么注入的,最终它是需要在页面里面通过 script 标签加载的

DOM 型 XSS

(纯前端涉及的主要是这里)

Dom Based XSS 并非按照“数据是否存在服务器端”来划分,DOM Based XSS 从效果上来说也是反射型 XSS。单独划分开来,是因为 DOM Based XSS 形成原因比较特别——通过修改页面的 DOM 节点形成的 XSS。

DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。

  • 在使用 ​.innerHTML​、​.outerHTML​、​document.write()​ 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 ​.textContent​、​.setAttribute()​ 等。实在需要使用,先转义。

Attributes

element.innerHTML = "<HTML> Tags and markup";

element.outerHTML = "<HTML> Tags and markup";

Methods

document.write("<HTML> Tags and markup");

document.writeln("<HTML> Tags and markup");

element.innerHTML = "<%=Encoder.encodeForJS(Encoder.encodeForHTML(untrustedData))%>";

element.outerHTML = "<%=Encoder.encodeForJS(Encoder.encodeForHTML(untrustedData))%>";

document.write("<%=Encoder.encodeForJS(Encoder.encodeForHTML(untrustedData))%>");

document.writeln("<%=Encoder.encodeForJS(Encoder.encodeForHTML(untrustedData))%>"

  • 如果用 Vue/React 技术栈,并且不使用 ​v-html​/​dangerouslySetInnerHTML​ 功能,就在前端 render 阶段避免 ​innerHTML​、​outerHTML​ 的 XSS 隐患。
  • DOM 中的内联事件监听器,如 ​location​、​onclick​、​onerror​、​onload​、​onmouseover​ 等,​​ 标签的 ​href​ 属性,JavaScript 的 ​eval()​、​setTimeout()​、​setInterval()​ 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。

场景实践

注意我这里的例子很多同学会困惑这个明明是储存型的或者是下面要讲的富文本的,为什么会放在这里,其实是因为最终页面使用的v-html

在发帖处输入

<a href="#" onclick="alert("1")">test</a>

使用这个库 js-xss,白名单,过滤掉 a 标签多余属性:https://github.com/leizongmin/js-xss/blob/master/lib/default.js

富文本(属于储存型,难点)

富文本和我们平常 XSS 案例不一样,不是简单的在输出位置做针对性过滤就行的,因为既然提供富文本编辑器,那么 肯定是内容需要支持某些标签输入输出,如果 XSS FILTER 直接干掉了标签,会影响到内容的展示,所以有必要对富文本情况拎出来单独处理。

富文本的 xss 防御比较多样化,除了上述的一些,还应注意使用白名单标签,避免使用黑名单标签。

处理富文本和输入检查是一致的,我们会对用户输入的信息添加一个白名单,比如在富文本里面允许用户输入​<img >​等标签,不允许输入​<script>​等标签,  建立标签白名单。用到下面这个库。

https://jsxss.com/zh/index.html

场景实践

默认未对 img 标签的 onerror 属性做过滤

<img/src=1 onerror=alert(1)>

使用 shtml 可以输出 HTML 的 tag,同时执行 XSS 的过滤动作,过滤掉非法的脚本。

shtml 在 xss 模块基础上增加了针对域名的过滤。

  • 默认规则
  • 自定义过滤项:http://jsxss.com/zh/options.html

例如只支持 a 标签,且除了 title 其他属性都过滤掉:​whiteList: {a: [‘title’]}​

options:

  • ​config.helper.shtml.domainWhiteList: []​ 可拓展 href 和 src 中允许的域名白名单。

注意,shtml 使用了严格的白名单机制,除了过滤掉 XSS 风险的字符串外,在默认规则外的 tag 和 attr 都会被过滤掉。

例如 HTML 标签就不在白名单中,

const html = '<html></html>';// html{{helper.shtml(html) }}// 输出空

常见的 ​data-xx​ 属性由于不在白名单中,所以都会被过滤。

所以,一定要注意 shtml 的适用场景,一般是针对来自用户的富文本输入,切忌滥用,功能既受到限制

解决方案

1、后端禁掉所有事件(通过拦截事件关键词,如果包含直接返回异常)

2、对于二次拼接的事件名处理

// 定义事件正则表达式

**private** **final** **static** List<String> eventTagList = Arrays.asList("onanimationcancel(.*?)=", "onanimationend(.*?)=", "onblur(.*?)=",

"oncancel(.*?)=", "oncanplay(.*?)=", "oncanplaythrough(.*?)=", "onchange(.*?)=", "onclick(.*?)=", "oncontextmenu(.*?)=",

"oncuechange(.*?)=", "ondblclick(.*?)=", "ondurationchange(.*?)=", "onended(.*?)=", "onerror(.*?)=", "onfocus(.*?)=",

"ongotpointercapture(.*?)=", "oninput(.*?)=", "oninvalid(.*?)=", "onkeydown(.*?)=", "onkeypress(.*?)=", "onkeyup(.*?)=",

"onload(.*?)=", "onloadeddata(.*?)=", "onloadedmetadata(.*?)=", "onloadend(.*?)=", "onloadstart(.*?)=", "onlostpointercapture(.*?)=",

"onmousedown(.*?)=", "onmouseenter(.*?)=", "onmouseleave(.*?)=", "onmousemove(.*?)=", "onmouseout(.*?)=", "onmouseover(.*?)=",

"onmouseup(.*?)=", "onpause(.*?)=", "onplay(.*?)=", "onpointercancel(.*?)=", "onpointerdown(.*?)=", "onpointerenter(.*?)=",

"onpointerleave(.*?)=", "onpointermove(.*?)=", "onpointerout(.*?)=", "onpointerover(.*?)=", "onpointerup(.*?)=", "onreset(.*?)=",

"onresize(.*?)=", "onscroll(.*?)=", "onselect(.*?)=", "onsubmit(.*?)=", "ontransitioncancel(.*?)=", "ontransitionend(.*?)=", "onwheel(.*?)=");

// 校验

**for** (String eventTag : eventTagList) {

scriptPattern = Pattern.compile(eventTag, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);

checkScriptPattern(scriptPattern, value);

}

**private** **static** **void** checkScriptPattern(Pattern scriptPattern, String value) **throws** KnightsException {

**if** (**null** == scriptPattern) {

**return**;

}

**boolean** find = scriptPattern.matcher(value).find();

**if** (find) {

logger.error("checkScriptPattern find, scriptPattern={}, value={}", scriptPattern.pattern(), value);

**throw** **new** KnightsException(RetCode.ERR_VP_CONTENT_ABNORMAL);

}

}

常用解决方案总结

1) CSP

CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

通常可以通过两种方式来开启 CSP:

  • 设置 HTTP Header 中的 Content-Security-Policy
  • 设置 meta 标签的方式

这里以设置 HTTP Header 来举例:

  • 只允许加载本站资源

Content-Security-Policy: default-src 'self'

  • 只允许加载 HTTPS 协议图片

Content-Security-Policy: img-src https://*

  • 允许加载任何来源框架

Content-Security-Policy: child-src 'none'

如需了解更多属性,请查看 Content-Security-Policy 文档

对于这种方式来说,只要开发者配置了正确的规则,那么即使网站存在漏洞,攻击者也不能执行它的攻击代码,并且 CSP 的兼容性也不错。

2)X-XSS-Protection

此 header 能在用户浏览器中开启基本的 XSS 防御。它不能避免一切 XSS 攻击,但它可以防范基本的 XSS。例如,如果浏览器检测到查询字符串中包含类似 ​<script>​ 标签之类的内容,则会阻止这种疑似 XSS 攻击代码的执行。这个 header 可以设置三种不同的值:​0​、​1​ 和 ​1; mode=block​。如果你想了解更多关于如何选择模式的知识,请查看 ​X-XSS-Protection​ 及其潜在危害 一文

  • Google Chrome’s XSS Auditor goes back to filter mode
  • Chrome removed the XSS Auditor
  • Firefox does not implement the XSSAuditor
  • Edge retired their XSS filter
  • OWASP ZAP deprecated the scan for the header
  • SecurityHeaders.com no longer scans for the header

3) 转义字符

用户的输入永远不可信任的,最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

function escape(str) {

str = str.replace(/&/g, '&amp;')

str = str.replace(/</g, '&lt;')

str = str.replace(/>/g, '&gt;')

str = str.replace(/"/g,'&quto;')

str = str.replace(/'/g,'&#39;')

str = str.replace(/`/g, '&#96;')

str = str.replace(/\//g, '&#x2F;')

return str

}

4)白名单

但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。

const xss = require('xss')

let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')

// -> <h1>XSS Demo</h1>&lt;script&gt;alert("xss");&lt;/script&gt;

console.log(html)

以上示例使用了 js-xss 来实现,可以看到在输出中保留了 h1 标签且过滤了 script 标签。

5) HttpOnly Cookie。

HttpOnly 是一个设置 cookie 是否可以被 javasript 脚本读取的属性,浏览器将禁止页面的 Javascript 访问带有 HttpOnly 属性的 Cookie。

这是预防 XSS 攻击窃取用户 cookie 最有效的防御手段。Web 应用程序在设置 cookie 时,将其属性设为 HttpOnly,就可以避免该网页的 cookie 被客户端恶意 JavaScript 窃取,保护用户 cookie 信息

const express = require('express');

const app = express();

app.use(function(req, res, next) {

res.setHeader('Content-Type', 'text/html;charset=utf-8');

// 模拟设置用户 tookie

// 可以把类似敏感信息都设置 httpOnly: true

res.cookie('token', 'zkskskdngqkkkgn245tkdkgj', { httpOnly: true});

next();

});

6) IP 访问限制

系统后台增加 ip 访问限制,主要是针对获取 cookie 伪造管理员登入方式

7)白名单无法解决的,禁止掉触发事件

8)验证码:防止脚本冒充用户提交危险操作。

9)输入内容长度控制

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

10)Javacript 过滤(上面已经讲了一些,涉及的内容很多,具体问题具体分析)

**web 安全从来是攻防的过程,攻击者的脑回路你总有猜不出来的时候,

所以总结来说,处理的时候可以有以下思路

  • 阻止输入,输入过滤
  • 后端入库前过滤
  • 后端输出前转义和过滤
  • 阻止输出(url 中的输出,css 中的输出,<script> 中的输出,html 属性中的输出,html 标签中的输出),阻止触发(事件触发、)

参考文献

  • Wikipedia. Cross-site scripting, Wikipedia.
  • OWASP. XSS (Cross Site Scripting) Prevention Cheat Sheet_Prevention_Cheat_Sheet), OWASP.
  • OWASP. DOM based XSS Prevention
  • OWASP. Use the OWASP Java Encoder-Use-the-OWASP-Java-Encoder), GitHub.
  • Ahmed Elsobky. Unleashing an Ultimate XSS Polyglot, GitHub.
  • Jad S. Boutros. Reducing XSS by way of Automatic Context-Aware Escaping in Template Systems, Google Security Blog.
  • Vue.js. v-html – Vue API docs, Vue.js.
  • React. dangerouslySetInnerHTML – DOM Elements, React.
  • XSS Filter Evasion Cheat Sheet:https://owasp.org/www-community/xss-filter-evasion-cheatsheet
退出移动版