实践理解 Web 安全 02 XSS 跨站脚本攻击

XSS攻击全解析:原理、分类与防御策略
本文详细讲解了XSS攻击的原理,包括反射型、DOM型和存储型的区别,以及服务端渲染的示例。同时阐述了预防XSS的策略,从输入过滤到转义和安全措施如CSP和X-XSS-Protection的应用。

Cross-Site Scripting (跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID等,进而危害数据安全。

Cross-Site Scripting 的英文首字母本应为 CSS,但因为 CSS 在网页涉及领域已经被广泛指层叠样式表(Cascading Style Sheets),所以将 Cross(意为“交叉”)改以交叉形的 “X” 作为缩写。

但早期的文件还是会使用 CSS 标识 Cross-Site Scripting。

XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。

默认网站只能运行站内的代码,XSS 攻击能够实现把外部 JavaScript 代码注入网站内部,形成攻击。

而由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。

在部分情况下,由于输入的限制,注入的恶意脚本比较短。但可以通过引入外部的脚本,并由浏览器执行,来完成比较复杂的攻击策略。

XSS 攻击

以服务端渲染(ASP)为例:

<h1>XSS</h1>
<!-- 假设 getParameter 是后端方法,获取 URL 参数的值;此处将值直接插入在HTML中 -->
<div><%= getParameter("keyword") %><div>

如果访问 http://xxx.com?keyword=<script>alert(123)</script>,就会在页面中插入一段脚本。

不过这类注入主要针对的是传统的 web 应用,大多数浏览器已经解决了这类方式注入(通过对 URL 编码或其他方式)。

不同浏览器对 '"<>等符号的 URL 编码机制不同,例如:

  • IE不会对 '"<>编码,但会判断URL上的 script

在这里插入图片描述

  • Chrome、FireFox 会对 '"<> 编码

在这里插入图片描述

以vue为例:

data: {
  // 这种方式需要HTML转义 `/`,所以无法实现注入。
  // content: '<script>alert(123)<\/script>',
  
  content: '<img src="xxx" οnerrοr="alert(123)" />'
}
<div v-html="content"></div>

页面加载图片失败,会触发 onerror 事件,进而执行恶意代码 alert(123)

实际情况,攻击者可能会将 alert(123) 换成获取客户端缓存(用户信息)的代码:

data: {
  content: `<img src="xxx" οnerrοr="localStorage.getItem('userInfo')" />`
}

XSS 攻击就是网页中有一块内容绑定了数据,这个数据按照 HTML 格式输出。

XSS 分类(攻击来源)

根据攻击来源,XSS 攻击可分为三类:

类型存储区(恶意代码存放的位置)插入点(由谁取得恶意代码,并插入到网页上)
反射型 XSSURLHTML
DOM 型 XSS后端数据库/前端存储/URL前端 JavaScript
存储型 XSS后端数据库HTML

反射型 XSS

反射型 XSS 就是利用 URL注入恶意代码。

攻击步骤

  1. 攻击者构造带有恶意代码的 URL
  2. 用户打开带有恶意代码的 URL 时,服务端将恶意代码从 URL 上取出,拼接在 HTML 中返回给浏览器。
  3. 浏览器解析页面,混在其中的恶意代码被执行。
  4. 恶意代码造成攻击。

示例

上面【XSS 攻击原理中的服务端渲染示例】就是反射型 XSS。

补充

反射型 XSS 主要就是将 URL 上的内容,未经处理(或处理的不完善)原封不动的插入到页面代码中。

攻击者一般会将带有 XSS 的链接发给用户,诱导点击。

这种漏洞需要满足的条件很多(诱导用户点击假链接、网站未作URL过滤、使用服务端渲染等),纯前端渲染中已经很少见,但由此可以衍生出来很多攻击点,需要开发者了解和注意。

DOM 型 XSS

攻击步骤

DOM 型 XSS 和 反射型 XSS 类似,只不过获取 URL 上的恶意代码是在客户端完成(前端 JavaScript)完成。

示例

<div v-html="$route.query.keyword"></div>

请求地址:

http://localhost:8080/#/?keyword=<a href="http://heiheihei.com">10元话费</a>
或者
http://localhost:8080/#/?keyword=<img src="xxx" onerror="alert(123)" />

在这里插入图片描述

存储型 XSS

这种攻击常见于带有用户保存数据的网站功能,如富文本编辑器发布文章、评论等。

攻击步骤

  1. 攻击者将恶意代码提交到目标网站的数据库中
  2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
  3. 浏览器解析页面,混在其中的恶意代码被执行。
  4. 恶意代码造成攻击。

示例

发布文章的表单:

<form action="">
  <div>
    <label>文章标题</label>
    <input name="title" />
  </div>
  <div>
    <label>文章内容</label>
    <!--
      内容一般使用富文本编辑器。
      富文本编辑器最终生成的内容:HTML 格式的字符串
      Web 后台会把文章内容存储到数据库
      当用户查看文章详情的时候,就会按照 HTML 格式渲染这个字符串(文章内容)
    -->
    <Editor name="content" />
  </div>
</form>

文章详情页面(以 Vue 为例):

<h1>{{ article.title }}</h1>
<div v-html="article.content"></div>

服务端渲染示例

反射型存储型 在纯前端渲染中已经很少见了,尤其是在大量使用第三方框架(Vue、React、Angular等)的前提下。因为它们已经帮我们对 XSS 攻击进行了处理。

反射型存储型 多发生于服务端渲染

前端开发者可以使用 nodejs 开启一个 web 服务,使用模板引擎渲染 html 内容实现服务端渲染(不要直接使用第三方框架,因为它们的模板引擎都对 XSS 做了处理)。

反射型 XSS 示例

1、初始化项目,安装依赖

mkdir demo-ssr
cd ./demo-ssr
npm init -y
npm i express art-template express-art-template

创建目录:

├─ views
│   └─ index.html # html 模板文件
├─ app.js # web 服务启动入口文件

2、开启 web 服务,响应 html 文件内容

views/index.html:

<!--
  这个页面是后端渲染出来的,整个页面都是模板字符串
-->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Server-Side Rendering: XSS</title>
</head>
<body>
  <form action="/" method="GET">
    <div>
      <label>输入搜索关键字:</label>
      <input name="keyword" type="text" />
    </div>
    <div>
      <button>搜索</button>
    </div>
  </form>

  <div>
    xxx 的搜索结果如下:
    <ul>
      <li>xxxx</li>
      <li>xxxx</li>
      <li>xxxx</li>
      <li>xxxx</li>
    </ul>
  </div>
</body>
</html>

app.js

const express = require('express')
const fs = require('fs');

const app = express()

app.get('/', (req, res) => {
  const html = fs.readFileSync('./views/index.html')
  res.end(html)
})

app.listen(3000, () => {
  console.log('running on http://localhost:3000')
})

nodemon ./app.js

在这里插入图片描述

3、使用模板引擎(art-template)渲染 HTML 内容,用数据替换模板内容

app.js:

const express = require('express')

const app = express()

app.engine('html', require('express-art-template'))

app.get('/', (req, res) => {
  // render 方法:
  // 1. 读取要渲染的页面模板内容(默认在 views 目录下查找文件)
  // 2. 渲染:用数据(第二个参数)替换模板中的内容
  // 3. 将渲染结果发送给客户端
  res.render('index.html', {
    keyword: 'bar'
  })
})

app.listen(3000, () => {
  console.log('running on http://localhost:3000')
})

index.html

<!--
  这个页面是后端渲染出来的,整个页面都是模板字符串
-->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Server-Side Rendering: XSS</title>
</head>
<body>
  <form action="/" method="GET">
    <div>
      <label>输入搜索关键字:</label>
      <input name="keyword" type="text" />
    </div>
    <div>
      <button>搜索</button>
    </div>
  </form>

  <div>
    <!-- 修改了这里 -->
    {{ keyword }} 的搜索结果如下:
    <ul>
      <li>xxxx</li>
      <li>xxxx</li>
      <li>xxxx</li>
      <li>xxxx</li>
    </ul>
  </div>
</body>
</html>

在这里插入图片描述

4、绑定表单输入的内容:

app.js

const express = require('express')

const app = express()

app.engine('html', require('express-art-template'))

app.get('/', (req, res) => {
  // render 方法:
  // 1. 读取要渲染的页面模板内容(默认在 views 目录下查找文件)
  // 2. 渲染:用数据(第二个参数)替换模板中的内容
  // 3. 将渲染结果发送给客户端
  res.render('index.html', {
    keyword: req.query.keyword, // 通过地址栏传递文本框输入的内容,例如 ?keyword=xxx
  })
})

app.listen(3000, () => {
  console.log('running on http://localhost:3000')
})

修改模板,根据搜索关键字展示内容:

<!--
  这个页面是后端渲染出来的,整个页面都是模板字符串
-->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Server-Side Rendering: XSS</title>
</head>
<body>
  <form action="/" method="GET">
    <div>
      <label>输入搜索关键字:</label>
      <input name="keyword" type="text" />
    </div>
    <div>
      <button>搜索</button>
    </div>
  </form>

  <!-- 当 keyword 有值时展示搜索结果 -->
  {{ if keyword }}
  <div>
    {{ keyword }} 的搜索结果如下:
    <ul>
      <li>xxxx</li>
      <li>xxxx</li>
      <li>xxxx</li>
      <li>xxxx</li>
    </ul>
  </div>
  {{ /if}}
</body>
</html>

在这里插入图片描述

5、原文输出

默认模板引擎会将内容进行转义处理,使用原文输出 @ 取消转义。

	<!-- 当 keyword 有值时展示搜索结果 -->
  {{ if keyword }}
  <div>
    <!-- 原文输出 -->
    {{@ keyword }} 的搜索结果如下:
    <ul>
      <li>xxxx</li>
      <li>xxxx</li>
      <li>xxxx</li>
      <li>xxxx</li>
    </ul>
  </div>
  {{ /if}}

在这里插入图片描述

以上是直接在 URL 上编写 keyword 的值,也可以直接在文本框中输入,点击【搜索】。

现在的页面就有 反射型 XSS 攻击风险。

存储型 XSS 示例

1、 创建文章详情页面 article.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>存储型 XSS</title>
  </head>
  <body>
    <h1>文章标题:{{ article.title }}</h1>
    <div>文章内容:</div>
    <div>{{@ article.content }}</div>
  </body>
</html>

2、服务端渲染文章详情页面,返回文章详情数据

app.js 添加内容:

// 模拟数据库存储的文章信息
const articles = [
  { id: 1, title: 'article 1', content: 'article 1 content' },
  { id: 2, title: 'article 2', content: 'article 2 content' },
  { id: 3, title: 'article 3', content: 'article 3 content' },
]

app.get('/article/:id', (req, res) => {
  const article = articles.find(item => item.id == req.params.id)
  if (!article) {
    return res.status(404).send('文章不存在')
  }
  res.render('article.html', {
    article
  })
})

在这里插入图片描述

假如攻击者发布的文章包含恶意代码:

// 模拟数据库存储的文章信息
const articles = [
  { id: 1, title: 'article 1', content: '<script>alert(123)</script>' },
  { id: 2, title: 'article 2', content: 'article 2 content' },
  { id: 3, title: 'article 3', content: 'article 3 content' },
]

在这里插入图片描述

XSS 总结

攻击注入方式

  • 用户的 UGC 内容:用户生成的内容
    • 用户通过文本框、编辑器等方式生成的内容
  • 来在第三方的链接
  • URL 参数
  • POST 参数
  • Referer(可能来自不可信的来源)
  • Cookie(可能来自其他子域注入)

注入点

  • 在 HTML 中内嵌的文本中,以 <script>标签注入恶意代码
<!-- dangerCode = '<script>alert(123)</script>' -->
<div>{{@ dangerCode }}</div>

<!-- 
渲染为:
<div><script>alert(123)</script></div>
-->
  • 在内联的 JavaScript 中,拼接的数据突破了原本的限制(字符串、变量、方法名等)
<!-- dangerCode = 'null; alert(123)' -->
<script>
var data = {{ dangerCode }}
</script>

<!-- 
渲染为:
<script>
var data = null; alert(123)
</script>
-->
  • 在标签属性中,恶意内容包含引号,从而突破属性值的限制,注入其他属性或标签
<!-- dangerCode = '"></a><a href="xxx.html' -->
<a href="{{ dangerCode }}">链接</a>

<!-- 
渲染为:
<a href=""></a><a href="xxx.html">链接</a>
-->
  • 在标签 hrefsrc等属性中,包含 javascript: 等可执行代码
<!-- dangerCode = 'javascript: alert(123)' -->
<a href="{{ dangerCode }}">链接</a>

<!-- 
渲染为:
<a href="javascript: alert(123)">链接</a>
-->
  • onloadonerroronclick等事件中,注入不受控制代码。
<!-- dangerCode = 'xxx" οnerrοr="alert(123)' -->
<img src="{{@ dangerCode }}" />

<!-- 
渲染为:
<img src="xxx" οnerrοr="alert(123)" />
-->
  • style 属性和标签中,包含类似 backgroung-image:url("javascript:...");的代码(新版本浏览器已经可以防范)
  • style属性和标签中,包含类似 expression(...)的 CSS 表达式代码(新版本浏览器已经可以防范)

总之,如果开发者没有将用户输入的文本进行合适的过滤,就贸然插入到 HTML 中,就很容易造成注入漏洞。

永远不要相信用户的输入

XSS 的危害

XSS 可以直接在网页中运行 web 脚本,所以可以进行很多恶意操作,例如:

  • 窃取 Cookie
  • 按键记录和钓鱼
  • 未授权操作
  • 获取页面数据
  • 获取本地存储数据
  • 劫持前端逻辑
  • 发送请求
    • 图片、表单等
  • 偷取网站任意数据
  • 偷取用户密码和登录状态
  • 欺骗用户
  • 利用虚假输入表单骗取用户个人信息
  • 显示伪造的文章或图片

XSS 的预防

通过前面的介绍可以得知,XSS 攻击有两大要素:

  1. 攻击者提交恶意代码
  2. 浏览器执行恶意代码

预防攻击者提交恶意代码

结论:在输入侧过滤能够在某些情况下解决特定的 XSS 问题,但会引入很大的不确定性和乱码问题。在防范 XSS 攻击时应避免此类方法。

针对第一个要素,也许我们可以在用户输入时过滤掉恶意代码。

但是这个方式范围有限,且有很多问题。

范围有限

  1. URL地址:我们无法阻止攻击者在地址栏直接添加恶意代码。
  2. 前端捕获用户输入过滤:对于提交用户输入(关键字搜索、发布文章等)的操作,我们也无法阻止攻击者绕过前端,直接构造请求。
  3. 服务端捕获用户输入过滤

所以在提交过程上,我们只能在服务端拦截用户输入,在存入数据库或响应给客户端前进行过滤,达到预防提交恶意代码的目的。

但是这仍然会因输出不确定,导致很多问题。

输出不确定

过滤恶意代码一般是对内容进行转义。

在服务端存储数据库前对输入进行过滤(转义),表示输出的内容一定是转义后带有“乱码符号”的字符串。

例如:5 < 7 转义后:5 &lt; 7

而这个内容不确定要输出到哪里:

  1. 提供给客户端,可能会经过 escapeHTML()编码
  2. 前端展示
    1. 作为 HTML 拼接页面
    2. 作为 Ajax 请求的数据,用于Vue 模板、内容长度计算、alert 等

结论

这个方式解决范围小,且有很大的不确定性问题,所以不建议在输入的时候对数据进行转义,而应该在输出的时候

对于明确的输入类型,例如数字、URL、电话号码、邮箱等内容,进行输入过滤还是必要的。

既然输入过滤并非完全可靠,我们就要通过防止第二个要素“浏览器执行恶意代码”来防范 XSS。这部分分为两类:

  1. 防止 HTML 中出现注入
  2. 防止 JavaScript 执行时,执行恶意代码

预防浏览器执行恶意代码

预防存储型和反射型 XSS 攻击

存储型和反射型 XSS 都是传统 web 应用使用服务端渲染方式,在服务端取出攻击者编写的带有恶意代码的 “数据” 后,插入到响应的 HTML 里,才被浏览器执行的。

预防这两个漏洞,有两个常见做法:

  • 改成纯前端渲染(客户端渲染),把代码和数据分隔开
  • 对 HTML 做充分转义
纯前端渲染

纯前端渲染就是客户端渲染,如 Vue React 开发的项目,过程:

  1. 浏览器加载静态 html,不包含任何后台数据
  2. 前端 JavaScript 通过 Ajax 获取业务数据
  3. 使用 DOM API 插入到页面中,避免直接拼接 html 或 js 代码

纯前端渲染还需注意避免 DOM 型 XSS 漏洞,参见下文。

在很多后台管理系统中,采用纯前端渲染是非常合适的。

但对于性能要求高、或有 SEO 需求的、需要使用服务端渲染的页面,仍要面对拼接 HTML 的问题。

转义 HTML

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

转义就是将敏感字符替换成 HTML 可以识别(JS 和 CSS 识别不了)的编码:

// 转义html
const escapeHTML = (str = '') => {
  str = str.replace(/</g, '&lt;')
  str = str.replace(/>/g, '&gt;')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, '&#39;')
  return str
}

// 转义js
const escapeJS = (str = '') => {
  str = str.replace(/"/g, '\\"')
  return str
}

大多数模板引擎都支持基本的 HTML 转义预防 XSS 攻击,不过通常只有一个规则,就是把 & < > " ' /这几个字符转义掉,虽然能起到一定的 XSS 防护作用,但并不完善:

XSS 安全漏洞简单转义是否有防护作用
HTML 标签文字内容
HTML 属性值
CSS 内联样式
内联 JavaScript
内联 JSON
跳转链接

所以要完善 XSS 的防护措施,要使用更完善更细致的转义策略,在不同的HTML上下文里要使用相应的转义规则。

使用专业的转义库,例如 js-xss,它既能在 node 环境(服务端)运行,也可以在浏览器环境(客户端)运行。

预防 DOM 型 XSS 攻击

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

本质上也是将恶意代码作为“代码”而不是“文本”去使用。

例如以下几个地方要特别注意使用的内容是否可信:

  • document API,拼接 HTML 代码:
    • innerHTML()
    • outerHTML()
    • document.write()
  • window API,将字符串参数作为 JS 代码执行:
    • setTimeOut()
    • setInterval()
  • JavaScript API,将字符串参数作为 JS 代码执行:
    • eval()
  • DOM 中的内联事件监听器:
    • location
    • onclick
    • onerror
    • onload
    • onmouseover
    • 等任意可插入不可信数据的地方
  • 标签属性:
    • <a> 标签的 `href``
    • <img> 标签的 src
    • 等任意可插入不可信数据的属性

对于文本内容应尽量使用 textContent()setAttribute() 等。

如果用 Vue/React 开发,并且不使用 v-html/dangerouslySetInnerHTML,就会在前端 render 避免 XSS 的隐患(内部使用 text 文本类的document API,而不是 html)。

其他 XSS 预防措施

CSP

CSP - MDN

内容安全策略(CSP)用于检测和减轻用于 Web 站点的特定类型的攻击,例如 XSS 和 数据注入等。

该安全策略的实现基于一个称作 Content-Security-Policy的 HTTP 头。

CSP就是通过 HTTP 协议,允许网站过滤掉一些内容,主要目标就是减少和报告 XSS 攻击。

Content-Security-Policy - MDN

HTTP 响应头 Content-Security-Policy允许站点管理者控制用户代理能够为指定的页面加载哪些资源。除了少数例外情况,设置的政策主要涉及指定服务器的源和脚本结束点。这将帮助防止跨站脚本攻击(XSS)。

配置方式:

  1. 服务器返回 Content-Security-PolicyHTTP头部
  2. 使用 <meta> 标签配置Content-Security-Policy

严格的 CSP 在 XSS 防范中可以起到以下作用:

  • 禁止加载外域代码,防止复杂的攻击逻辑
  • 禁止外域提交,网站被攻击后,用户的数据不会被泄漏到外域
  • 禁止内联脚本执行
  • 禁止未授权的脚本执行
  • 合理使用上报可以及时发现 XSS,利于尽快修复问题

注意:IE 并不兼容此功能。

X-XSS-Protection

X-XSS-Protection - MDN

和 CSP 一样,也是通过配置 HTTP 响应头 X-XSS-Protection预防 XSS 攻击。

它主要靠 浏览器 来检测 来自 URL 上的 反射型 XSS 攻击,当检测到 XSS 攻击时,浏览器将停止加载页面。

相比 CSP 已经过时,现代浏览器几乎不支持,但仍可以作为不支持 CSP 的旧版浏览器的一种保护措施。

例如,京东就配置了该响应头:

在这里插入图片描述

1;mode=block

启用XSS过滤。 如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。

其他主动预防

Web 没有绝对的安全,只能尽可能地增加安全策略,增加攻击难度,例如:

  • 对于不受信任的输入,限定一个合理的长度
  • 使用验证码,防止脚本冒充用户提交危险操作
  • 限制访问 Cookie,设置 SecureHttpOnly

XSS 检测

学会了 XSS 以及如何预防,还需要学会如何检测,尽可能确保安全。

可以手动发起 XSS 攻击,也有自动扫描工具寻找 XSS 漏洞。

详细参考:前端安全系列(一):如何防止XSS攻击? 中的 《XSS 的检测》

参考

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值