安全指南
概览
ECharts 旨在提供丰富且灵活的可视化能力。虽然绝大多数 API 不需要特殊的安全考量,但有几个 API 是例外。例如,配置项 tooltip.formatter 接受原始 HTML 字符串,允许完全控制组件的内容和布局;配置项 title.link 直接使用提供的 URL 字符串,而不进行自动过滤。虽然这种灵活性非常强大,但如果输入来自不可信的源,可能会产生安全风险。下面列出了这些 API,并就如何安全地使用这些功能提出了建议。
任何安全问题都可以根据 ASF 安全页面进行报告。
注意:本文档面向 ECharts API 调用者。代码贡献者安全检查清单是提交 ECharts Pull Request 前使用的另一个文档。它不面向 ECharts API 调用者,但如果有兴趣也可以参考。
安全模型和检查清单
ECharts 专注于可视化逻辑。与大多数前端 UI 库一样,它通常假设输入源自可信源,并且不执行自动过滤。事实上,ECharts 本身无法妥善地过滤不可信的输入,因为它无法判断哪些输入是不可信的,也没有通用的过滤规则适用于所有情况。然而,ECharts 应该清楚地识别哪些 API(特别是 ECharts 配置项)在特定用例中需要安全相关的预处理或考量。鉴于 ECharts 配置项数量庞大,在每种情况下对所有输入进行预处理是不切实际且不必要的。
ECharts 使用 Canvas 或 SVG 渲染,除了几个允许 HTML 渲染的特殊组件(例如 tooltip, dataView)。ECharts API 接受非 JS 函数输入和 JS 函数输入。JS 函数输入旨在被执行。大多数非 JS 函数输入(例如提供渲染的纯文本)仅被视为数据,从本质上防止了代码求值和执行。因此,它们通常不需要针对恶意代码进行过滤。然而,有几个 API 允许将潜在的不安全内容(例如原始 HTML 或原始 URL)嵌入到页面中。这些 API 功能强大,但如果输入源自不可信的源,则容易受到跨站脚本 (XSS) 和相关攻击。
总的来说,如果没有涉及不可信内容,这些注入漏洞就不会出现。 不可信内容是指源自无法完全控制,或者可以被用户或外部系统修改或注入的源。开发者必须假设在 HTML、CSS 和 JS 中直接使用这些内容是不安全的。例如,由用户生成或从客户端接收的内容就是不可信的。然而,处理用户提供的内容通常是不可避免的。例如,通过自定义格式化函数在基于 HTML 的 tooltip 中渲染从数据库获取的用户数据。在这种情况下,需要额外的处理来确保正确性和安全性——主要是通过 HTML 转义,另外当存在无法转义且源自不可信源的部分时,需要通过过滤 (Sanitization) 来处理。
在部署图表之前,请审阅此检查清单以确保你的使用是安全的:
| API | 潜在风险和建议 |
|---|---|
| 配置项 tooltip.formatter · formatter 允许 HTML 字符串或 DOM 元素输入,它们随后会直接在提示框内渲染,此处需要考虑 XSS 风险。(例外情况):直接设置为 formatter 的字符串被视为简单的模板,随后在内部与数据结合。tooltip.renderMode: 'richText' 是另一种用于样式的模板语法。两者都是内部实现的,可以防止注入。配置项 toolbox.feature.dataView.optionToContent 配置项 toolbox.feature.dataView.title 配置项 toolbox.feature.dataView.lang · tooltip.dataView 面板完全在 HTML 中渲染。HTML 字符串的某些部分允许通过这些 API 进行自定义。 | 应考虑 XSS 风险。在大多数情况下,仅靠 HTML 转义就足够了。但如果任何未转义的部分源自不可信的源,则需要更多措施(例如:过滤、沙箱)。 请参阅“安全地传递原始 HTML”部分以获取安全使用建议。 |
| 配置项 tooltip.extraCssText · extraCssText 接受原始 CSS 样式字符串,随后(通过 DOM API)直接附加到 tooltipEl.style.cssText。(例外情况):当使用 tooltip.renderMode: 'richText' 时,此选项不适用。 | 如果输入来自可信源则安全;否则,需要进行仔细评估。 详见“安全地传递内联 CSS”部分。 |
| 配置项 title.link 配置项 title.sublink 配置项 series-treemap.data.link 配置项 series-sunburst.data.link · 它们直接接受这些链接的原始 URL。 | 如果输入来自可信源则安全;否则,应考虑 XSS 风险。 请参阅“安全地传递原始 URL”部分以获取安全使用建议。 |
| 配置项 toolbox.feature.saveAsImage.name 配置项 toolbox.feature.saveAsImage.type 配置项 title[0].text · 下载文件名由 {name}.{type} 组装,不进行验证或过滤。如果未提供 name,历史上曾使用 title[0].text(如果有),尽管不建议这样做。 | 请参阅“安全地传递下载文件名”部分以获取安全使用建议。 |
| 配置项 dataset.transform 类型为 'filter' 且带有 config.reg · 过滤器转换的 config 支持 reg 键(字符串或 RegExp),用于通过正则表达式匹配过滤行。字符串值直接编译为 RegExp 并针对每一行执行,不验证模式复杂性、长度或执行时间。 | 如果 config(包括 reg)可能来自不可信源,则存在 ReDoS(正则表达式拒绝服务攻击)风险,可能会导致浏览器标签页卡死或阻塞服务端渲染。详见“数据集过滤器转换和 reg 配置项”部分。 |
| 所有 JS 函数输入(回调函数) | 通常这不需要担心,除非特殊需求涉及不可信的代码。 详见“安全地传递 JS 函数”部分。 |
安全地传递原始 HTML
“安全模型和检查清单”一节列出了直接接受原始 HTML 的 API。不可信的 HTML 可能会导致 XSS 和相关攻击,因此在将内容传递给 ECharts 之前需要进行额外的处理。下面介绍了几种常用的缓解方法——“HTML 转义”、“过滤 (Sanitization)”和“沙箱 (Sandboxing)”。在大多数情况下,仅靠“HTML 转义”就足够了,除非未转义的内容来自不可信源。
HTML 转义
在将数据组装成 HTML 字符串之前,对数据进行 HTML 转义始终是必要的——这不仅是为了安全,也是为了显示的基本正确性。
一个典型且最简单的 HTML 转义实现是进行以下字符转换:
'&' => '&'
'<' => '<'
'>' => '>'
'"' => '"'
"'" => ''
它消除了标记字符的功能,从而关闭了代码注入(例如 <script>...</script>)的攻击途径,无论内容是可信还是不可信的。
例如:
// Incorrect and unsafe.
formatter: params => {
const { name, value } = params;
// May cause incorrect rendering if `name` or `value` contain functional
// charactors like '<', '>', etc.
// Additionally, it introduces XSS risks if `name` or `value` come from
// untrusted sources, where malicious code may be injected into that strings.
return `${name}, <b>${value}</b>`;
}
// Correct and safe.
formatter: params => {
const { name, value } = params;
return `${echarts.format.encodeHTML(name)}, <b>${echarts.format.encodeHTML(value)}<b/>`;
}其他方法,如使用 DOM API .textContent = ,也可以转义 HTML。
在大多数用例中,不可信内容(例如用户提供的文本)仅用于显示为纯文本,而标记令牌(即不应转义的部分,如 HTML 标签或属性)则完全由应用程序所有者控制,因此是可信的。在这种情况下,只要所有不可信内容都被妥善转义,仅靠 HTML 转义就是预防 XSS 的有效且简单的方法。
过滤 (Sanitization)
某些用例需要将不可信的标记令牌解释为实际标记。例如,来自数据库的文本可能包含样式或功能标签(例如 <em>, <a href="...">),这些标签旨在被解释而非显示为纯文本。另一个例子是,允许用户或不可信源提供定义结构和样式的 HTML 模板,这些模板随后与数据内容结合生成最终可渲染的 HTML 并传递给 ECharts。
在这些情况下,安全风险会增加。可以应用过滤 (Sanitization) 来减轻这些风险,前提是不允许执行嵌入的 JS 和 CSS 代码。过滤器根据预定义的白名单过滤 HTML 内容——例如,删除 <script> 和 <style> 块、<link> 元素、内联 CSS、诸如 onclick 之类的事件处理程序属性,以及使用 javascript:/data: 协议的 URL。建议使用维护良好且被广泛采用的过滤器,而不是编写自己的正则表达式或进行手动字符串操作。
根据产品需求和威胁模型,过滤可以在客户端、服务器端或两者同时执行。例如,对于源自客户端的内容(例如由用户提交的内容),仅依靠客户端过滤是不够的,因为攻击者可以绕过客户端直接向服务器提交构造好的有效负载。例如,一个在线可视化编辑器允许用户以所见即所得 (WYSIWYG) 的方式编写文章,用户可以从几个内置的 HTML 片段/模板或 JS 函数(例如用于 tooltip.formatter 或 label.formatter)中进行选择。如果所选或生成的 HTML 文本或 JS 函数文本从客户端发送到服务器并持久化到数据库而没有任何额外处理,攻击者可以模拟网络请求来注入恶意代码。稍后,当这些选项被检索并传递给 chart.setOption() 时,恶意代码就会执行。针对这种情况有一些推荐的缓解措施:
- 仅持久化对这些内置片段/模板或 JS 函数的引用(ID),而不是客户端提供的原始代码。
- 如果为了表达力允许用户提供片段(超出内置选择),可以引入一些第三方字符串模板库来防止注入。
- 如果用户提供的片段必须允许 HTML(超出上述方法),请在持久化之前强制执行严格的服务器端过滤或验证。这应包括删除所有 JS、CSS 和其他潜在不安全内容。此外,考虑使用沙箱 iframe 来限制任何残留安全问题的潜在影响。
通过过滤实现足够的安全性有时并不容易。它需要正确的配置,必须与浏览器变化保持同步,并且通常需要与其他防御机制结合使用,才能对大多数现实世界的用例做到“足够安全”。HTML 极其复杂——不可信内容中允许的功能越多,引入的潜在攻击途径就越多。
沙箱 (Sandboxing)
如果需要执行不可信的代码,或者认为其他措施不足,沙箱 iframe 可以提供更高级别的安全性,就像 JSFiddle 和 CodePen 等服务所使用的那样。
安全地传递内联 CSS
虽然 HTML 安全讨论中已经涵盖了 CSS 安全问题(参见“安全地传递原始 HTML”部分),但本节重点讨论仅接受内联 CSS 字符串的 API(那些通过 DOM API .style.cssText = 修改 style 属性的 API),这些 API 列在“安全模型和检查清单”部分。
如果内联 CSS 字符串完全来自可信源(例如,它们是应用程序的一部分),则安全考量极少——这也是最常见的情况。
否则,不可信的 CSS 可能会导致攻击。一些广泛采用的 HTML 过滤器支持 CSS 过滤,但默认可能会删除所有 CSS,因为妥善过滤 CSS 非常复杂。因此,接受不可信的 CSS 需要根据具体用例进行缜密的评估。
安全地传递原始 URL
虽然 HTML 安全讨论中已经涵盖了 URL 安全问题(参见“安全地传递原始 HTML”部分),但本节重点讨论仅接受 URL 字符串的 API,这些 API 列在“安全模型和检查清单”部分。
如果 URL 字符串完全来自可信源(例如,它们是应用程序的一部分),则安全考量极少。
否则,不可信的 URL 字符串可能会导致攻击,例如通过使用 javascript: 或 data: 等协议来执行恶意代码。因此,在将它们传递给 ECharts 之前,应强制执行过滤,通常是通过针对指定的白名单验证协议来实现。
安全地传递下载文件名
目前,仅 saveAsImage 功能接受文件名作为输入。如果此输入源自可信源、不超过长度限制且仅包含 ASCII 字母字符,则可以认为是安全的。
否则,虽然现代浏览器在处理未经清洗的下载文件名方面已有显著改进(例如,路径遍历序列 ../、..\\ 会被剥离,特殊字符会被正确处理),但仍存在一些风险,包括保留字符和长度限制的行为不一致。此外,旧版客户端或浏览器的行为不明确且不够健壮。因此,对于这些不可信的输入,需要进行过滤或预处理。
数据集过滤器转换和 reg 配置项
数据集过滤器转换 (dataset filter transform) 的 config 支持 reg 键,通过针对维度值匹配正则表达式来过滤数据行。reg 接受字符串或 RegExp;字符串会被直接编译为 RegExp,并且每一行都会通过 test() 进行匹配,没有对模式复杂性、长度或执行时间的验证或限制。这种设计支持可序列化的配置(例如来自 JSON 或 API 的 reg: '^asdf$'),但当 config 来自不可信源时,它会引入 ReDoS(正则表达式拒绝服务攻击)风险。
风险
如果攻击者可以影响图表配置项(例如保存的仪表板或分析配置),他们可以提供具有灾难性回溯特性的模式(例如嵌套量词 ^(a+)+$)以及长的非匹配输入。单次 test() 调用就可能导致指数级的 CPU 占用。
- 客户端:渲染图表的浏览器标签页可能会卡死并需要强制关闭。
- 服务端:使用 ECharts 服务端渲染 (SSR) 时,一个恶意请求就可能阻塞 Node.js 事件循环并影响所有并发用户。
- 持久性:如果恶意配置被存储(例如在仪表板中),每个打开该视图的用户都可能触发 DoS。
这不涉及数据泄露或修改;影响主要在于可用性。
建议
- 如果
dataset.transform的config(包括reg)完全来自可信源(例如你的应用程序或受控的后端),则无需额外措施。 - 如果
config可能来自不可信源(例如用户输入、外部 API、未经验证的数据库内容):- 在将配置项传递给 ECharts 之前,对
reg字符串强制执行最大长度和复杂性检查(例如拒绝具有嵌套量词或其他已知灾难性回溯结构的模式),或者仅允许简单模式的白名单。 - 或者,考虑使用具有线性时间保证的正则表达式引擎(例如服务器上的 RE2),或为匹配添加超时机制(例如在具有时间限制的 Web Worker 中运行该工作),以降低 ReDoS 风险。
- 如果产品不需要不可信方提供正则模式,请禁止来自外部配置的
reg,仅允许代码中定义的安全模式。
- 在将配置项传递给 ECharts 之前,对
安全地传递 JS 函数
ECharts 配置项(即传递给 chart.setOption() 的输入)主要是声明式的,但某些配置项接受 JS 函数(回调)以提供更大的表达力和灵活性。例如 label.formatter、axisTick.interval 等。在大多数用例中,这些 JS 函数配置项是应用程序源代码本身的一部分,因此是完全可信的,不会引入安全风险。
然而,某些产品可能允许 JS 函数配置项源自不可信源(例如最终用户)。允许这样做会同时引入安全风险和维护成本。从本质上讲,这种情况与允许在不可信的原始 HTML 中执行代码具有相同级别的风险,可以使用类似的进近方式进行缓解,如在“安全地传递原始 HTML”中所讨论的那样。