XSS是什么?
XSS: Cross-Site Scripting Attack(跨站脚本攻击)是一种注入攻击,攻击者通过在目标网站上注入恶意脚本,伪装成受害用户的身份执行用户才能执行的操作。
按照字面简称,应该叫做CSS的才是是吧?可是已经有个CSS(Cascading Style Sheets)了,所以这里叫做XSS。
XSS是如何工作的?
一般情况下,一个站点对于前端数据
(前端参数或者用户输入)不做校验,将前端的内容在某个条件下直接使用HTML页面进行了渲染,如果这部分前端数据
含有恶意的Javascript
内容,那么会被浏览器直接执行,这段恶意的Javascript
可以模仿当前用户做一些只有该用户能做的操作。
我这边给它整理了两个必要条件:
- 注入恶意
Javascript
- 浏览器执行恶意
Javascript
找个有漏洞的地方注入Javascript
是基础,但是如果只有恶意的Javascript
注入,没有真正执行脚本的地方,那么也不构成威胁。
XSS的分类
按照注入恶意脚本的方式可以分为以下几种:
- 反射型XSS
- 存储型XSS
- DOM型XSS
上面说的一些笼统的概念,可能看了没有什么感觉,下面会根据三种分类,分别举例说明,让大家更有实感。
反射型XSS
特点:前端传参没有经过校验,直接渲染(执行参数里面的恶意代码)
1、多出现在URL传参的条件下
2、需要引导用户主动点击恶意URL
3、后端从URL中取出数据拼接到页面
流程如下图:
举个例子,我们以前用到的JSP页面可能有如下代码:
直接在将URL
的参数keyword
渲染到页面上。
正常情况,URL
为http://xxx/search?keyword=someword
,那么下面的div内容就是:
<div>
您搜索的关键词是:someword
<div>
那如果是有人恶意伪造一个URL
,比如是
http://xxx/search?keyword="><script>alert('XSS');</script>
那么他就把keyword
后面的全部内容注入到页面上,最终页面div内容是:
<div>
您搜索的关键词是:"><script>alert('XSS');</script>
<div>
对于很多浏览器而言,可能就是直接弹出一个对话框了,表明这段注入的Javascript
执行了。
可能有的朋友要问了:
就这?这也没什么啊,弹一个对话框而已,不影响。
这里我想说的是,上面只是一个例子,它表示一个本来不应该被执行的代码段被执行了,那如果这个代码并不只是弹出一个对话框,而是一些恶意操作呢?
下面就有一个真实案例:2011年6月28日的新浪微博的反射型XSS蠕虫案例
在2011年6月28日这天,当大家在正常使用新浪微博的时候,有一个病毒正在快速传播
- 20:00,有一部分人在私信里面看到了比较刺激的连接,点击它
- 20:14,开始有大量带V的认证用户中招转发蠕虫,特点是:关注一个叫做
hellosamy
的用户,同时给自己的粉丝发私信,私信中携带这个链接,这样开始大量传播 - 20:30,2kt.cn中的病毒页面无法访问
- 20:32,新浪微博中hellosamy用户无法访问
- 21:02,新浪漏洞修补完毕
注意,大家并不是没有防病毒心理,比如一个陌生连接,我们一般是不点击的,但是大家也看到了,上面这个链接,他开头就是
http://weibo.com/pub/star
,一看就是微博自己的连接,新浪微博官方的连接总不是病毒吧?于是就点击了。
上面的这个就是中了反射型XSS的招了,主要是微博自己间上面URL参数(就是pub/star/g/
后面的内容)没有经过过滤,直接渲染到页面,那么里面内嵌的恶意脚本www.2kt.cn/images/t.js
就被浏览器执行了,这个脚本通过cookie
拿到用户信息,再利用微博公开API给粉丝发私信,这样就指数性的传播开来了。
该病毒作者明显只是想秀一下本事,他的名字
hellosamy
就是在致敬第一个XSS蠕虫
作者samy
, 蠕虫可以快速自我繁殖,这个又是依靠XSS
做的,所以叫做XSS蠕虫
存储型XSS
特点:用户数据(包括恶意数据)通过一些接口被上传至服务器,在另一个地方不经校验的被渲染出来
1、攻击者先把恶意代码注入到库中
2、多存在于UGC地方
3、正常用户正常访问就中招
举个例子:
有一个论坛,用户可以做评论,可以刷评论,用户A
在评论中输入了一些恶意代码
<script>alert(“您的机器被攻击了”)</script>
用户B
在正常浏览页面,刷评论的时候,却弹出来一个框(黑人问号脸?)
DOM型XSS
特点:前端页面没有经过校验的直接渲染一些前端数据,仅限于前端内容
1、多出现在URL传参的条件下
2、需要引导用户主动点击恶意URL
3、前端JS从URL中取出数据放入页面
举个例子:
如果有如下代码,它是直接使用Javascript
从前端URL中获取数据并且渲染出来
如果直接修改这个URL,将参数改变,那么就可以注入Javascript
代码了
伪造一个URL后,发给别的人,别人点击后,就会弹出对话框了
上面几种XSS介绍完,这里做一个总结:
XSS的危害
介绍了上面几个类型的XSS,我们能感知到它的危害很大,具体有哪些危害呢?我这里大致总结一下:
- 冒充或伪装成受害用户
- 执行用户能够执行的任何操作
- 读取用户能够访问的任何数据
- 捕获用户的登录凭据
- 将特洛伊木马功能注入网站
如何防治?
XSS危害比较大,十年前是互联网病毒的重灾区,我这里大致根据三个方面总结进行防治:
- 数据转义
- 内容过滤以及白名单
- 使用CSP
数据转义
一般XSS
都是注入一些特殊的标签内容,可以通过内容转义来避免
Vue中使用{{}}而不是v-html加载数据
vue中的大括号会把数据解释为普通文本。通常如果要解释成html代码则要用v-html。而此指令相当于innerHTML。虽然像innerHTML一样不会直接输出script标签,但也可以输出img,iframe等标签。
下面是官网上的教程:
React中自动转义
React 在渲染 HTML 内容和渲染 DOM 属性时都会将 "'&<> 这几个字符进行转义,转义部分源码如下:
for (index = match.index; index < str.length; index++) {
switch (str.charCodeAt(index)) {
case 34: // "
escape = '"';
break;
case 38: // &
escape = '&';
break;
case 39: // '
escape = ''';
break;
case 60: // <
escape = '<';
break;
case 62: // >
escape = '>';
break;
default:
continue;
}
}
这段代码是 React 在渲染到浏览器前进行的转义,可以看到对浏览器有特殊含义的字符都被转义了,恶意代码在渲染到 HTML 前都被转成了字符串,如下:
// 一段恶意代码
<img src="empty.png" onerror ="alert('xss')">
// 转义后输出到 html 中
<img src="empty.png" onerror ="alert('xss')">
这样就有效的防止了 XSS 攻击。
React中的JSX 语法
JSX 实际上是一种语法糖,Babel 会把 JSX 编译成 React.createElement() 的函数调用,最终返回一个 ReactElement,以下为这几个步骤对应的代码:
// JSX
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
// 通过 babel 编译后的代码
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
// React.createElement() 方法返回的 ReactElement
const element = {
$$typeof: Symbol('react.element'),
type: 'h1',
key: null,
props: {
children: 'Hello, world!',
className: 'greeting'
}
...
}
我们可以看到,最终渲染的内容是在 Children 属性中,那了解了 JSX 的原理后,我们来试试能否通过构造特殊的 Children 进行 XSS 注入,来看下面一段代码:
const storedData = `{
"ref":null,
"type":"body",
"props":{
"dangerouslySetInnerHTML":{
"__html":"<img src=\"empty.png\" onerror =\"alert('xss')\"/>"
}
}
}`;
// 转成 JSON
const parsedData = JSON.parse(storedData);
// 将数据渲染到页面
render () {
return <span> {parsedData} </span>;
}
这段代码中, 运行后会报以下错误,提示不是有效的ReactChild
。
Uncaught (in promise) Error: Objects are not valid as a React child (found: object with keys {ref, type, props}). If you meant to render a collection of children, use an array instead.
那究竟是哪里出问题了?我们看一下 ReactElement 的源码:
const symbolFor = Symbol.for;
REACT_ELEMENT_TYPE = symbolFor('react.element');
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 这个 tag 唯一标识了此为 ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 元素的内置属性
type: type,
key: key,
ref: ref,
props: props,
// 记录创建此元素的组件
_owner: owner,
};
...
return element;
}
注意到其中有个属性是 $$typeof
,它是用来标记此对象是一个ReactElement
,React
在进行渲染前会通过此属性进行校验,校验不通过将会抛出上面的错误。React
利用这个属性来防止通过构造特殊的Children
来进行的XSS
攻击,原因是$$typeof
是个Symbol类型
,进行JSON 转换后会Symbol
值会丢失,无法在前后端进行传输。如果用户提交了特殊的Children
,也无法进行渲染,利用此特性,可以防止存储型的XSS
攻击。
服务器中转义
服务器中,比如Java服务器,可以在特定敏感接口(一般是包含UGC内容的接口,即用户输入的内容)中做内容转义。
在Apache
的commons-lang
中,提供了一个比较好的转义工具类StringEscapeUtil
,他可以防止各种注入类攻击(SQL注入和XSS等)
import org.apache.commons.lang.StringEscapeUtils;
public class EscapeString {
public static void main(String[] args) throws Exception {
String str = "APEC召开时不让点柴火做饭";
System.out.println("用escapeJava方法转义之后的字符串为:"+StringEscapeUtils.escapeJava(str));
System.out.println("用unescapeJava方法反转义之后的字符串为:"+StringEscapeUtils.unescapeJava(StringEscapeUtils.escapeJava(str)));
System.out.println("用escapeHtml方法转义之后的字符串为:"+StringEscapeUtils.escapeHtml(str));
System.out.println("用unescapeHtml方法反转义之后的字符串为:"+StringEscapeUtils.unescapeHtml(StringEscapeUtils.escapeHtml(str)));
System.out.println("用escapeXml方法转义之后的字符串为:"+StringEscapeUtils.escapeXml(str));
System.out.println("用unescapeXml方法反转义之后的字符串为:"+StringEscapeUtils.unescapeXml(StringEscapeUtils.escapeXml(str)));
System.out.println("用escapeJavaScript方法转义之后的字符串为:"+StringEscapeUtils.escapeJavaScript(str));
System.out.println("用unescapeJavaScript方法反转义之后的字符串为:"+StringEscapeUtils.unescapeJavaScript(StringEscapeUtils.escapeJavaScript(str)));
/**输出结果如下:
用escapeJava方法转义之后的字符串为:APEC\u53EC\u5F00\u65F6\u4E0D\u8BA9\u70B9\u67F4\u706B\u505A\u996D
用unescapeJava方法反转义之后的字符串为:APEC召开时不让点柴火做饭
用escapeHtml方法转义之后的字符串为:APEC召开时不让点柴火做饭
用unescapeHtml方法反转义之后的字符串为:APEC召开时不让点柴火做饭
用escapeXml方法转义之后的字符串为:APEC召开时不让点柴火做饭
用unescapeXml方法反转义之后的字符串为:APEC召开时不让点柴火做饭
用escapeJavaScript方法转义之后的字符串为:APEC\u53EC\u5F00\u65F6\u4E0D\u8BA9\u70B9\u67F4\u706B\u505A\u996D
用unescapeJavaScript方法反转义之后的字符串为:APEC召开时不让点柴火做饭**/
}
}
内容过滤以及白名单
除了转义,我们还可以通过内容过滤来防止XSS
,将可能涉及到XSS
的数据进行过滤,再加上白名单机制,只允许一些指定的标签
或者其他特殊字符
通过,其他的标签
等就会被剔除掉,这样内容会少一些,少的都是有危险的部分。
前端使用xss框架过滤
在项目中,XSS的安全漏洞很容易出现,例如在聊天模块和富文本模块很容易出现。
有时候你想实现富文本编辑器里编辑html内容的业务。可是又担心XSS恶意脚本的注入。此时可以使用一个xss工具。网址:https://github.com/leizongmin/js-xss
下载xss
npm i xss -S
(1)在页面中引入资源且生成XSS过滤器,对内容进行过滤
var xss = require("xss")
const option={} //自定义设置
const myxss = new xss.FilterXSS(option);
const line='<script type="text/javascript">alert(1);</script>'
var html = myxss.process(line);
console.log(html); //输出:<script type="text/javascript">alert(1);</script>
(2)如果我想不过滤img标签的onerror属性,或者不过滤style标签。通过设置whiteList可选择性的保留特定标签及其属性,例如:
const option={
whiteList:{
img:['src','onerror'] //img标签保留src,onerror属性
style:['type'] //style标签默认是不在whileList属性里的,现在添加上去
}
}
const myxss = new xss.FilterXSS(option);
letline='<img src="./123.png" onerror="alert(1);" alt="123">'
let html = myxss.process(line);
console.log(html); //输出:<img src="./123.png" onerror="alert(1);">
line='<style type="text/css">color:white;</style>'
html = myxss.process(line);
console.log(html); //输出:<style type="text/css">color:white;</style>
xss默认的whiteList可以通过console.log(xss.whiteList)显示。
(3)如果想彻底过滤掉类似script,noscript标签,option可如下设置:
const option={
stripIgnoreTagBody: ["script","noscript"],
}
const myxss = new xss.FilterXSS(option)
let line='<script type="text/javascript">alert(1);</script>'
let html = myxss.process(line)
console.log(html.length) //输出0,即html被转化为空字符串
line='<noscript>123</noscript>'
html = myxss.process(line)
console.log(html.length) //输出0,即html被转化为空字符串
后端使用JSoup白名单过滤
Jsoup 使用标签 白名单 的机制用来进行防止 XSS 攻击,假设白名单中只允许 p 标签存在,此时在一段 HTML 代码中,只能存在 p 标签 ,其他标签将会被清除只保留被标签所包裹的内容:
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Whitelist;
/**
* 描述: 过滤 HTML 标签中 XSS 代码
*/
public class JsoupUtil {
/**
* 使用自带的 basicWithImages 白名单
* 允许的便签有 a,b,blockquote,br,cite,code,dd,dl,dt,em,i,li,ol,p,pre,q,small,span,strike,strong,sub,sup,u,ul,img
* 以及 a 标签的 href,img 标签的 src,align,alt,height,width,title 属性
*/
private static final Whitelist whitelist = Whitelist.basicWithImages();
/** 配置过滤化参数, 不对代码进行格式化 */
private static final Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
static {
// 富文本编辑时一些样式是使用 style 来进行实现的
// 比如红色字体 style="color:red;"
// 所以需要给所有标签添加 style 属性
whitelist.addAttributes(":all", "style");
}
public static String clean(String content) {
return Jsoup.clean(content, "", whitelist, outputSettings);
}
}
使用CSP
CSP 的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置。
CSP 大大增强了网页的安全性。攻击者即使发现了漏洞,也没法注入脚本,除非还控制了一台列入了白名单的可信主机。
两种方法可以启用 CSP:
- 通过HTTP头信息的
Content-Security-Policy
- 通过网页的标签
通过HTTP头信息的Content-Security-Policy
Content-Security-Policy: script-src 'self'; object-src 'none';
style-src cdn.example.org third-party.org; child-src https:
通过网页的标签
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">
上面代码中,CSP 做了如下配置。
- 脚本:只信任当前域名
<object>
标签:不信任任何URL,即不加载任何资源- 样式表:只信任
cdn.example.org
和third-party.org
- 框架(
frame
):必须使用HTTPS协议加载 - 其他资源:没有限制
启用后,不符合 CSP 的外部资源就会被阻止加载。
Chrome的报错信息:
总结
XSS就是通过服务器/客户端中没做严格校验的漏洞,将恶意Javascript
代码注入到站点中,漏洞会在渲染页面的时候执行这些恶意脚本,攻击者可以伪造用户来访问用户数据,使用用户身份做一些危害用户数据安全的事情。
因为其操作简单,危害很大,互联网开始的十多年是网络安全的重灾区。
要应对XSS,我们主要就是做好数据验证
,包括数据转义和数据过滤(白名单),或者使用浏览器标准CSP等方法。
参考:
https://portswigger.net/web-security/cross-site-scripting
https://juejin.cn/post/6874743455776505870
https://ghthou.github.io/2018/01/13/Jsoup-防止-XSS-攻击/
https://www.cnblogs.com/wuguanglin/p/XSS.html
评论区