# 什么是 XSS 攻击

XSS 全称是跨站脚本攻击(Cross-Site Scripting),为了避免和 CSS 冲突,第一个简写字母改为 X。它是一种代码注入形式的攻击,攻击者通过向网站注入恶意脚本,获取用户敏感信息,进行恶意操作。

攻击者可以通过哪些方式注入恶意脚本呢?

  • 用户输入
  • URL 参数
  • 请求参数

我们先来看几个 XSS 攻击的小例子:

例子一:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <p>请输入您的评价内容:</p>
  <input id="input" type="text">
  <button onclick="submit()">提交</button>
  <script>
    function submit() {
      document.write(input.value)
    }
  </script>
</body>
</html>

当我们在输入框输入 <script>alert('xss')</script>,并点击提交按钮后,页面弹出了 alert 弹窗。

用 innerHTML 插入文本到网页中有可能成为网站攻击的媒介,从而产生潜在的安全风险问题。所以HTML 5 中指定不执行由 innerHTML 插入的 <script>标签。

例子二:

下面代码的页面地址为:http://127.0.0.1:5500/index.html?redirect_to=javascript:alert(%27XSS%27)

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <a id="redirect">跳转。。。</a>
  <script>
    const href = getUrlParam('redirect_to')
    if (href) {
      redirect.href = href;
    }

    function getUrlParam(name) {
      const obj = {};
      const search = location.search;

      if(!search) return '';

      const arr = search.split('?')[1].split('&')
      arr.forEach(item => {
        const res = item.split('=');
        obj[res[0]] = res[1];
      })
      return obj[name]
    }
  </script>
</body>

</html>

当我们点击 a 标签的时候页面会弹出 XSS 。

案例三:

网页地址 http://127.0.0.1:5500/1.html?goUrl=javascript:alert(%27xss%27)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    location.href = getUrlParam('goUrl')

    function getUrlParam(name) {
      const obj = {}
      const search = location.search.split('?')[1];
      if (!search) return '';

      search.split('&').forEach(item => {
        const arr = item.split('=');
        obj[arr[0]] = arr[1] || ''
      })
      return obj[name]
    }
  </script>
</body>
</html>

当页面加载完后,会弹出 xss 弹窗。

# XSS 分类

# 存储型

前端将恶意脚本提交到了后端,后端将恶意脚本存储到了数据库,当客户端执行这些脚本后,达到攻击的目的。

存储型 XSS 一般发生在用户提交数据的工具,如:论坛发帖、商品品论、用户私信等。

# 反射型

恶意脚本做为请求参数的一部分发送到服务器,服务器解析参数后,返回给客户端,浏览器执行恶意脚本,达到攻击的目的。

反射型 XSS 漏洞一般发生在通过 URL 传递参数的功能,如搜索、跳转等。

可以看到存储型 XSS 将恶意脚本存储到了数据库,反射型 XSS 将恶意脚本存储到了 URL 中。

# DOM型

# XSS 防范

# 输入过滤

对用户输入的内容进行过滤,需要前后端一起做,因为前端不安全,万一攻击者绕过了前端过滤,恶意数据就被直接提交到了后端。

function htmlEscape(text) {
  return text.replace(/[<>"&]/g, function (match, pos, originalText) {
    switch (match) {
      case "<":
        return "&lt;";
      case ">":
        return "&gt;";
      case "&":
        return "&amp;";
      case "\"":
        return "&quot;";
    }
  });
}

即便后端对输入内容进行了过滤也会存在问题。比如 1>2 这个转义后,变成了 1&gt;2,后端把这个转义后的字符串返回给了前端,如果前端直接把这个字符串展示在 html 中,显示是正常的。如果 JS 代码中需要使用这个字符串,那就有问题了。

// 我们期望的字符串是 '1>2',而我们从后端拿到的是 '1&gt;2'
const a = '1&gt;2';

所以不能依赖于对输入过滤,

# CSP

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

该安全策略的实现基于一个称作 Content-Security-Policy 的 HTTP 首部。可以起到以下作用:

  • 禁止加载外域代码,防止复杂的攻击逻辑。
  • 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
  • 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
  • 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
  • 合理使用上报可以及时发现 XSS,利于尽快修复问题。

# 限制输入长度

对用户输入限制合理的输入长度,可以增加 XSS 攻击的难度。

# HttpOnly

cookie 设置了 HttpOnly 属性后,通过 JS 就无法访问 cookie 了,可以有效防止 XSS 攻击。

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly

# XSS 检测

我们可以使用下面这个神奇的代码来手动进行 XSS 漏洞检测,这里简单举个例子,具体使用请参考这篇文章 Unleashing an Ultimate XSS Polyglot (opens new window)

jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e

# XSS 总结

XSS 防范需要前后端共同努力,不能单靠一方。后端主要方法存储型和反射型的 XSS 攻击,前端主要防范 DOM 型的 XSS 攻击。在对数据进行转义时一定要注意,不要出现乱码。

# 参考文章

前端安全系列(一):如何防止XSS攻击? (opens new window)