- 前端
- UI组件
【免费下载链接】BOOTSTRA.386
A vintage 1980s DOS inspired Twitter Bootstrap theme
导读:BOOTSTRA.386 是基于 Bootstrap 5.3 的复古 DOS 风格主题,其 Tooltip 与 Popover 组件默认启用内置内容净化器(Sanitizer),会自动剔除 HTML 内容中未被白名单允许的元素,从而阻断javascript:伪协议等典型 XSS 攻击向量。本文以 v5.3.1/site/layouts/partials/callouts/info-sanitizer.md 中那段组件提示为线索,结合仓库内的官方文档、净化器源码 与单元测试,完整讲解默认白名单的结构、净化流程的底层实现,以及如何安全地扩展或替换内置净化器。读完你将能正确配置allowList、sanitize与sanitizeFn三项核心选项,并理解为什么这些选项不能通过 data 属性传入。
一、提示语出处:哪些组件默认启用净化器
在 BOOTSTRA.386 的 v5.3.1 文档体系中,info-sanitizer.md是一个被多处以 callout(提示框)形式复用的片段,原文为:
By default, this component uses the built-in content sanitizer, which strips out any HTML elements that are not explicitly allowed. See the sanitizer section in our JavaScript documentation for more details.
(默认情况下,该组件使用内置内容净化器,它会剔除任何未显式允许的 HTML 元素。更多细节请参见 JavaScript 文档中的净化器章节。)
该片段目前被两处组件文档引用:
- Tooltips 文档:位于组件总览(Overview)之后,提醒使用者 Tooltip 默认净化 HTML 选项;
- Popovers 文档:位置与作用完全相同。
它指向的"净化器章节"位于 JavaScript 文档,其中明确写道:Tooltips 和 Popovers 使用内置净化器来清洗所有接受 HTML 的选项。也就是说,这两个组件是净化器的实际消费方,而这套机制本身在 v5.3.1 中被统一实现在js/src/util/下,并在构建产物 js/dos.js 中随主题一起分发。
二、为什么需要净化器:从 XSS 说起
Tooltip 与 Popover 允许开发者通过title、content等选项注入 HTML 富文本(需配合html: true开启)。如果这些内容直接原样写入 DOM,攻击者就可以构造:
<a href="javascript:alert(document.cookie)">点我</a>一旦用户触发悬浮或点击,javascript:伪协议便会在当前页面上下文执行任意脚本,这是最经典的存储型/注入型 XSS。净化器的职责正是"非白名单不放行":不认识的标签直接删除,危险的属性(尤其是可执行脚本的 URL)一律剥离,从而在渲染前把恶意载荷清除干净。
三、默认白名单(DefaultAllowlist)的结构
净化器的放行依据是DefaultAllowlist白名单对象,完整定义在 sanitizer.js 中:
const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i export const DefaultAllowlist = { // 任意元素上允许的全局属性 '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], a: ['target', 'href', 'title', 'rel'], area: [], b: [], br: [], col: [], code: [], div: [], em: [], hr: [], h1: [], h2: [], h3: [], h4: [], h5: [], h6: [], i: [], img: ['src', 'srcset', 'alt', 'title', 'width', 'height'], li: [], ol: [], p: [], pre: [], s: [], small: [], span: [], sub: [], sup: [], strong: [], u: [], ul: [] }解读这个白名单需要注意三个设计要点:
*键是全局属性集:class、dir、id、lang、role以及所有aria-*属性(由正则/^aria-[\w-]*$/i匹配)被允许出现在任何放行元素上;- 键值即元素各自的专属属性:例如
a元素只允许target、href、title、rel四个属性;img元素允许src、srcset、alt、title、width、height;其余大多数标签(如div、span、strong)属性列表为空,意味着只放行全局属性; - 白名单同时支持正则表达式:
ARIA_ATTRIBUTE_PATTERN就是典型示例,开发者可以像它一样向白名单追加自定义正则来校验属性名。
注意:DefaultAllowlist中没有script、style、iframe、object、form等高风险标签,因此它们会被整体移除。
四、净化流程的源码级剖析
净化入口是 sanitizeHtml(unsafeHtml, allowList, sanitizeFunction),其执行逻辑分为四步:
4.1 空串短路与自定义函数优先
export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { if (!unsafeHtml.length) { return unsafeHtml } if (sanitizeFunction && typeof sanitizeFunction === 'function') { return sanitizeFunction(unsafeHtml) } // ... }空字符串直接原样返回(不做无谓的 DOM 解析);若调用方提供了sanitizeFunction,则完全交由外部函数处理,内置逻辑不再执行。
4.2 用 DOMParser 解析为 DOM 树
const domParser = new window.DOMParser() const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html') const elements = [].concat(...createdDocument.body.querySelectorAll('*'))净化基于浏览器原生DOMParser将 HTML 字符串解析成真实 DOM,再遍历body下的所有元素节点。这里特意使用了原生解析器而非正则,因为浏览器解析能处理各种畸形 HTML 与实体编码(如javascript:这类编码后的javascript:),避免被混淆载荷绕过。
4.3 元素级过滤:标签必须命中白名单
for (const element of elements) { const elementName = element.nodeName.toLowerCase() if (!Object.keys(allowList).includes(elementName)) { element.remove() continue } // ... }标签名不在白名单中的元素会被remove()直接移除。注意这里移除了整个节点,其内部子元素也随之消失。
4.4 属性级过滤:放行 + URL 安全校验
const allowedAttribute = (attribute, allowedAttributeList) => { const attributeName = attribute.nodeName.toLowerCase() if (allowedAttributeList.includes(attributeName)) { if (uriAttributes.has(attributeName)) { return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue)) } return true } // 检查是否有正则表达式命中该属性名 return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp) .some(regex => regex.test(attributeName)) }属性的允许条件有二:要么名字直接出现在该元素(或*全局)的属性列表中;要么能命中列表中的某个正则(例如aria-*)。而一旦属性是 URL 型属性,还要通过第二道关卡——SAFE_URL_PATTERN:
const uriAttributes = new Set([ 'background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href' ]) const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i该正则在开头显式排除了javascript:协议(源码注释中标注参考了 Angular 的 URL 净化实现),同时放行http:、https:、ftp:、mailto:、tel:、data:、相对路径、锚点(#anchor)等安全形式。凡是href、src等属性值命中javascript:及其大小写变体、空白混淆变体,属性都会被removeAttribute删除。
最终函数返回createdDocument.body.innerHTML,即净化后的 HTML 字符串,再交由上层写入组件模板。
五、净化器如何进入 Tooltip / Popover 渲染链路
净化并非孤立函数,它经由TemplateFactory(模板工厂)接入组件渲染链路:
- template-factory.js 中定义了三个相关默认值:
sanitize: true(默认开启净化)、allowList: DefaultAllowlist(默认白名单)、sanitizeFn: null(默认无自定义函数); - 模板工厂在渲染模板和注入内容时统一调用 _maybeSanitize():
_maybeSanitize(arg) { return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg }也就是说,sanitize: false时会跳过净化;否则进入sanitizeHtml。组件模板本身(如 Tooltip 的template配置)与注入的content都会经过同一净化管线。
在 tooltip.js 中,Default配置直接复用了这套默认值;popover.js 则通过...Tooltip.Default展开继承,并额外定义了content: ''选项——它同样接受 HTML 并被净化。
安全细节:tooltip.js 定义了一个禁止通过 data 属性配置的名单:
const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])在 _getConfig() 中,凡是从元素data-*属性读取到这三个键都会被删除,防止攻击者通过data-bs-sanitize="false"之类的 HTML 属性静默关闭安全机制。官方在 Popovers 文档 中也明确提示:出于安全原因,sanitize、sanitizeFn和allowList不能通过 data 属性提供,只能通过 JavaScript 配置对象传入。单元测试 tooltip.spec.js 也验证了即使元素上写了data-bs-sanitize="false",实例的_config.sanitize仍为true。
六、实战一:如何扩展默认白名单
官方文档(javascript.md Sanitizer 章节)给出了标准的扩展写法——直接操作插件默认配置对象:
const myDefaultAllowList = bootstrap.Tooltip.Default.allowList // 允许 table 元素 myDefaultAllowList.table = [] // 允许 td 元素,并放行其>const yourTooltipEl = document.querySelector('#yourTooltip') const tooltip = new bootstrap.Tooltip(yourTooltipEl, { sanitizeFn(content) { return DOMPurify.sanitize(content) } })从 sanitizeHtml 的实现可以看到,一旦sanitizeFunction是函数,内置的DOMParser路径会被完全跳过(单元测试 sanitizer.spec.js 用 spy 验证了传入自定义函数后DOMParser.prototype.parseFromString不会被调用)。因此sanitizeFn适合接入任意你信任的净化库,同时保留allowList之外的灵活策略。
八、实战三:按需关闭净化
仅在你完全信任传入内容、且确需保留内联事件等富特性时,才考虑关闭净化:
const tooltip = new bootstrap.Tooltip(el, { html: true, sanitize: false })设置sanitize: false后,template-factory.js 中的_maybeSanitize将直接返回原始内容。关闭净化等于把 XSS 防线交给调用方自己,只应应用于受信任内容(例如你硬编码的静态文案),绝不要用于任何用户输入。相关行为同样有测试佐证(template-factory.spec.js 验证了sanitize: false时不净化模板)。
九、安全模型总结与测试佐证
综合源码与测试,这套净化机制的安全模型可归纳为三层防线:
| 防线 | 机制 | 源码位置 |
|---|---|---|
| 标签层 | 未列入allowList的元素整体移除 | sanitizer.js |
| 属性层 | 属性名必须命中白名单或正则 | sanitizer.js |
| URL 层 | javascript:等危险协议被SAFE_URL_PATTERN拦截 | sanitizer.js |
仓库中的 sanitizer.spec.js 为每一层提供了直接证据:
- 合法 URL 全放行:
http(s)://、ftp://、mailto:、tel:、sip:、data:image/png、#anchor、相对路径等 17 类 URL 均保留(L13-L49); - 危险 URL 全拦截:
javascript:alert(7)、大小写变体、空白/换行混淆、HTML 实体编码、jav\u0000ascript等 12 种变体均被剔除(L51-L82); - 危险标签移除:
<script>alert(7)</script>被完整删除(L122-L132); - 可重复净化:同一模板多次净化结果一致,无副作用(L153-L161)。
十、查阅与扩展指引
- 提示语组件本体:v5.3.1/site/layouts/partials/callouts/info-sanitizer.md
- 官方净化器章节:v5.3.1/site/content/docs/5.3/getting-started/javascript.md
- 组件选项表格:Tooltip 的
allowList选项见 tooltips.md,Popover 的sanitize/sanitizeFn/allowList限制见 popovers.md - 核心实现:js/src/util/sanitizer.js、js/src/util/template-factory.js
- 消费方实现:js/src/tooltip.js、js/src/popover.js
- 单元测试:js/tests/unit/util/sanitizer.spec.js、js/tests/unit/util/template-factory.spec.js、js/tests/unit/tooltip.spec.js
一句话收束:BOOTSTRA.386 的 Tooltip 与 Popover 默认开启的净化器,是一套"白名单标签 + 白名单属性 + 安全 URL 校验"的三段式内置防线——日常开发中你通常无需改动任何配置即可获得基础 XSS 防护;需要自定义富文本能力时,请通过 JavaScript 配置对象扩展allowList或注入sanitizeFn,并牢记这三个选项永远不应暴露给不可信的 data 属性。
- 前端
- UI组件
【免费下载链接】BOOTSTRA.386
A vintage 1980s DOS inspired Twitter Bootstrap theme
相关推荐
clipboard.js安全加固:内容净化与XSS防护
clipboard.js安全加固:内容净化与XSS防护 你还在为剪贴板功能担惊受怕?3步实现企业级安全防护 当用户点击"复制"按钮时,你是否想过这简单的操作可能
前端BOOTSTRA.386 v5.3.1 定制体系详解:从 Sass 定制全景到 CSP 内嵌 SVG 的治理方案
BOOTSTRA.386 v5.3.1 定制体系详解:从 Sass 定制全景到 CSP 内嵌 SVG 的治理方案 本篇技术指南以 BOOTSTRA.386 仓库
前端UI组件Symfony HTML净化:XSS防护与内容安全过滤
Symfony HTML净化:XSS防护与内容安全过滤 在Web应用开发中,用户输入的HTML内容往往是安全漏洞的重灾区。跨站脚本攻击(XSS)通过注入恶意代码
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考