XSS 的攻击和防御

505 阅读4分钟

Cross Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使其在用户的浏览器上运行。通过这些恶意的脚本,攻击者可获取用户的敏感信息如:Cookie、Session等。

Cross Site Scripting 的英文首字母缩写应为 CSS,但是因为其和我们的样式语法 CSS(Cascading Style Sheets)一样,所以将 CSS 改成 XSS。

类型主要包括:

  • 反射型 XSS
  • 存储型 XSS
  • DOM 型 XSS

反射型 XSS

  • 攻击者构造出特殊的 URL,其中包含恶意代码。
  • 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器
  • 用户浏览器接收到响应后解析执行,从而窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站的数据接口。

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

场景

客户端

<!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>
  <form action="/" method="GET">
    <div>
      <label for="name">输入内容</label>
      <input type="text" id="name" name="keyword">
    </div>
    <button>搜索</button>
  </form>

  {{if keyword }}
    <div>搜索内容:{{@ keyword }}</div>
    <ul>
      <li>搜索结果xxx</li>
      <li>搜索结果xxx</li>
      <li>搜索结果xxx</li>
    </ul>
  {{/if}}
</body>
</html>

服务端

const express = require('express')
const app = express()

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

app.get('/', (req, res) => {
  res.render('xss.html', {
    keyword: req.query.keyword
  })
})

app.listen(3000, () => {
  console.log('服务器启动成功:http://localhost:3000')
})

攻击过程:前端 -> 后端 -> 前端

前端输入搜索内容

后端返回数据,前端完整显示

存储型 XSS

  • 攻击者将恶意代码提交到目标网站的数据库中
  • 攻击者打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器
  • 用户浏览器接收到响应后解析执行,从而窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站的数据接口。
  • 存储型 XSS 和 反射型 XSS 的主要区别:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL里

常见于评论、文章详情等地方。攻击者将包含恶意代码的文章数据发布,存储到数据库中。其他用户访问时就会触发该恶意代码

场景

客户端

<!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>文章/评论内容</h1>
  {{if title }}
    <div>{{ title }}</div>
    <div>{{@ content }}</div>
  {{/if}}
</body>
</html>

服务端

const express = require('express')
const app = express()

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

// // 反射型
// app.get('/', (req, res) => {
//   res.render('xss.html', {
//     keyword: req.query.keyword
//   })
// })

// 模拟数据库
const db = [
  {
    id: 1,
    title: '标题1',
    content: '内容1'
  },
  {
    id: 2,
    title: '标题2',
    content: '内容1 <script>alert("xss")</script>'
  }
]

// 存储型
app.get('/:id', (req, res) => {
  const data = db[req.params.id] || {}
  res.render('xss2.html', data)
})

app.listen(3000, () => {
  console.log('服务器启动成功:http://localhost:3000')
})

攻击过程:前端 -> 后端 -> 数据库 -> 后端 -> 前端

攻击者发布了 A 文章,用户访问了该文章

访问了具有攻击代码的文章

DOM 的 XSS

  • 攻击者构造出特殊的 URL,其中包含恶意代码
  • 用户打开带有恶意代码的 URL
  • 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行
  • 从而窃取用户信息获取冒充用户行为

DOM 型 XSS 跟前两种 XSS 的区别: DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器执行完成,属于前端自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。

场景

HTML 属性

<img src="xxx" onerror="alert('xss')" />

<body onload="alert('xss')"></body>

<a href="javascript:alert('xss')"></a>

XSS 防御

通过上述的 XSS 步骤我们知道 XSS 攻击主要是通过两大要素:

  • 攻击者提交恶意代码
  • 浏览器执行恶意代码

输入过滤:

  • 由前端过滤,提交后端。如果攻击者绕过前端过滤,直接构造请求一样可以将恶意代码提交
  • 如果由后端在写入数据库前过滤,如 10 < 20,在入库是转义就会变成 10 &lt; 20。此时输出给浏览器是可以直接识别的,但是如果输出给其他客户端就会出现乱码的情况。

结论:不建议在输入的时候对数据进行转义,而是在输出的时候

防御 反射型 XSS 和 存储型 XSS

常见方法:

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

纯前端渲染

  • 通过静态 HTML,加载 JavaScript 脚本
  • 通过 JavaScript 脚本请求 Ajax,来加载业务数据
  • 调用 DOM API 将数据更新到页面中

通过 DOM API 如:设置内容文本(.innerText),设置属性(.setAttribute),设置样式(.style)等。纯前端需要注意避免 DOM 型 XSS 攻击(例如 onload 事件、href 中的 javascript:xx 等)

转义 HTML

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

如:把 & < > " ' / 等字符进行转义,但是这不够完善。所有我们需要借助转义库,来进行完善转义。

例如 Node.js 中常用的转义库 js-xss

预防 DOM XSS 攻击

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

使用 .innerTHML.outerHTMLdocument.write() 须谨慎,尽量使用 .textContent.setAttribute

CSP

CSP (Content-Security-Policy) 的简称。是一个额外的安全层,用于检测和削弱某些特定的攻击。