From 1ae1e3409c0c9ff0898509d48ada98cc986becab Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Mon, 24 Nov 2025 16:29:51 +0800 Subject: [PATCH 1/5] feat(plugin-markdown-chart): avoid XSS attack BREAKING CHANGES: echart variables in scripts are changed, script blocks MUST be manually allowed --- docs/.vuepress/configs/plugins.ts | 5 ++ .../.vuepress/echarts-snippets/bar.snippet.md | 4 +- .../echarts-snippets/line.snippet.md | 4 +- .../plugins/markdown/markdown-chart/README.md | 11 +++ .../markdown/markdown-chart/echarts.md | 2 +- .../plugins/markdown/markdown-chart/README.md | 11 +++ .../markdown/markdown-chart/echarts.md | 2 +- .../src/client/components/ECharts.ts | 6 +- .../src/node/markdown-it-plugins/chartjs.ts | 71 ++++++++++++++++--- .../src/node/markdown-it-plugins/echarts.ts | 65 +++++++++++++++-- .../src/node/markdownChartPlugin.ts | 28 +++++++- .../plugin-markdown-chart/src/node/options.ts | 24 +++++++ 12 files changed, 210 insertions(+), 23 deletions(-) diff --git a/docs/.vuepress/configs/plugins.ts b/docs/.vuepress/configs/plugins.ts index 8db3d76851..8bc0374745 100644 --- a/docs/.vuepress/configs/plugins.ts +++ b/docs/.vuepress/configs/plugins.ts @@ -52,6 +52,11 @@ export const plugins = [ markmap: true, mermaid: true, plantuml: true, + DANGEROUS_ALLOW_SCRIPT_EXECUTION: true, + DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST: [ + 'plugins/markdown/markdown-chart/echarts', + 'zh/plugins/markdown/markdown-chart/echarts', + ], }), markdownExtPlugin({ gfm: true, diff --git a/docs/.vuepress/echarts-snippets/bar.snippet.md b/docs/.vuepress/echarts-snippets/bar.snippet.md index fc86d44007..3cde016c6f 100644 --- a/docs/.vuepress/echarts-snippets/bar.snippet.md +++ b/docs/.vuepress/echarts-snippets/bar.snippet.md @@ -60,13 +60,13 @@ const run = () => { for (let i = 0; i < data.length; i++) data[i] += Math.round(Math.random() * Math.random() > 0.9 ? 2000 : 200) - echarts.setOption({ + myChart.setOption({ series: [{ type: 'bar', data }], }) } const timeId = setInterval(() => { - if (echarts._disposed) { + if (myChart._disposed) { clearInterval(timeId) return diff --git a/docs/.vuepress/echarts-snippets/line.snippet.md b/docs/.vuepress/echarts-snippets/line.snippet.md index fa49b616ae..8fe1a922e1 100644 --- a/docs/.vuepress/echarts-snippets/line.snippet.md +++ b/docs/.vuepress/echarts-snippets/line.snippet.md @@ -77,7 +77,7 @@ const option = { ], } const timeId = setInterval(() => { - if (echarts._disposed) { + if (myChart._disposed) { clearInterval(timeId) return } @@ -86,7 +86,7 @@ const timeId = setInterval(() => { data.shift() data.push(randomData()) } - echarts.setOption({ + myChart.setOption({ series: [{ data }], }) }, 1000) diff --git a/docs/plugins/markdown/markdown-chart/README.md b/docs/plugins/markdown/markdown-chart/README.md index 4adda5d4d8..9f3389cfcd 100644 --- a/docs/plugins/markdown/markdown-chart/README.md +++ b/docs/plugins/markdown/markdown-chart/README.md @@ -125,3 +125,14 @@ export default { - Type: `boolean | MarkdownItPlantumlOptions[]` - Details: Whether to enable PlantUML support. Can accept configuration options for advanced usage. + +### DANGEROUS_ALLOW_SCRIPT_EXECUTION + +- Type: `boolean` +- Details: Whether to allow script execution in charts. + +### DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST + +- Type: `string[] | '*'` +- Default: `[]` +- Details: A list of allowed script sources when script execution is enabled. Use `'*'` to allow all sources. diff --git a/docs/plugins/markdown/markdown-chart/echarts.md b/docs/plugins/markdown/markdown-chart/echarts.md index 4174b95e33..84d6b7f1e6 100644 --- a/docs/plugins/markdown/markdown-chart/echarts.md +++ b/docs/plugins/markdown/markdown-chart/echarts.md @@ -73,7 +73,7 @@ If you can generate your chart data easily, you can just provide echarts config If you need to use script to get the data, you can use `js` or `javascript` code block. -We will expose the echarts instance as `echarts` in the script, and you are expected to assign the echarts option object to `option` variable. Also, you can assign `width` and `height` variable to set the chart size. +We will expose the echarts lib as `echarts` and the instance as `myChart` in the script, and you are expected to assign the echarts option object to `option` variable. Also, you can assign `width` and `height` variable to set the chart size. ````md ::: echarts Title diff --git a/docs/zh/plugins/markdown/markdown-chart/README.md b/docs/zh/plugins/markdown/markdown-chart/README.md index 1b8d150bb5..5b15ba1cda 100644 --- a/docs/zh/plugins/markdown/markdown-chart/README.md +++ b/docs/zh/plugins/markdown/markdown-chart/README.md @@ -125,3 +125,14 @@ export default { - 类型:`boolean | MarkdownItPlantumlOptions[]` - 详情:是否启用 PlantUML 支持。可以接受配置选项以供高级使用。 + +### DANGEROUS_ALLOW_SCRIPT_EXECUTION + +- 类型:`boolean` +- 详情:是否允许在图表中执行脚本。这可能会带来安全风险,请谨慎使用。 + +### DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST + +- 类型:`string[] | '*'` +- 默认:`[]` +- 详情:当启用脚本执行时,允许的脚本源列表。使用 `'*'` 允许所有源。 diff --git a/docs/zh/plugins/markdown/markdown-chart/echarts.md b/docs/zh/plugins/markdown/markdown-chart/echarts.md index 61992c2981..4f9c3c3451 100644 --- a/docs/zh/plugins/markdown/markdown-chart/echarts.md +++ b/docs/zh/plugins/markdown/markdown-chart/echarts.md @@ -75,7 +75,7 @@ export default { 如果你需要通过脚本来获取数据,你可以使用 `js` 和 `javascript` 的代码块。 -我们将通过 `echarts` 变量暴露 ECharts 实例,并且你应该将 Echart 配置赋值给 `option` 变量。同时,你也可以赋值 `width` 和 `height` 来设置图表大小。 +我们将通过 `echarts` 变量暴露 ECharts 库以及 `myChart` 暴露 ECharts 实例,并且你应该将 Echart 配置赋值给 `option` 变量。同时,你也可以赋值 `width` 和 `height` 来设置图表大小。 ````md ::: echarts Title diff --git a/plugins/markdown/plugin-markdown-chart/src/client/components/ECharts.ts b/plugins/markdown/plugin-markdown-chart/src/client/components/ECharts.ts index 53cb418d54..d1365e19d2 100644 --- a/plugins/markdown/plugin-markdown-chart/src/client/components/ECharts.ts +++ b/plugins/markdown/plugin-markdown-chart/src/client/components/ECharts.ts @@ -1,6 +1,7 @@ import { LoadingIcon, decodeData } from '@vuepress/helper/client' import { useDebounceFn, useEventListener } from '@vueuse/core' import type { EChartsOption, EChartsType } from 'echarts' +import type * as Echarts from 'echarts' import type { PropType, VNode } from 'vue' import { defineComponent, @@ -29,12 +30,14 @@ const AsyncFunction = (async (): Promise => {}).constructor const parseEChartsConfig = ( config: string, type: 'js' | 'json', + echarts: typeof Echarts, instance: EChartsType, ): Promise => { if (type === 'js') { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call const runner = AsyncFunction( 'echarts', + 'myChart', `\ let width,height,option,__echarts_config__; { @@ -46,7 +49,7 @@ return __echarts_config__; ) // eslint-disable-next-line @typescript-eslint/no-unsafe-call - return runner(instance) as Promise + return runner(echarts, instance) as Promise } return Promise.resolve({ option: JSON.parse(config) as EChartsOption }) @@ -116,6 +119,7 @@ export default defineComponent({ const { option, ...size } = await parseEChartsConfig( decodeData(props.config), props.type, + echarts, instance, ) diff --git a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts index 4a27cd37a5..a528f59a6b 100644 --- a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts +++ b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts @@ -1,16 +1,46 @@ import { container } from '@mdit/plugin-container' import { encodeData } from '@vuepress/helper' -import type { PluginSimple } from 'markdown-it' +import type { PluginWithOptions } from 'markdown-it' +import { colors } from 'vuepress/utils' + +export interface ChartJSPluginOptions { + /** + * Allow executing custom scripts inside Chart.js blocks. + * 允许在 Chart.js 块内执行自定义脚本。 + * + * @default false + */ + allowScripts?: boolean + + /** + * Allow all scripts to be executed inside Chart.js blocks. + * 允许在 Chart.js 块内执行所有脚本。 + * + * @default false + */ + allowAll?: boolean + + /** + * List of files allowed to execute scripts inside Chart.js blocks. + * 允许在 Chart.js 块内执行脚本的文件列表。 + */ + allowList: Set +} /** * Chart.js markdown-it plugin * * Chart.js markdown-it 插件 */ -export const chartjs: PluginSimple = (md) => { +export const chartjs: PluginWithOptions = ( + md, + options, +) => { + const { allowScripts, allowAll, allowList = new Set() } = options! + container(md, { name: 'chartjs', - openRender: (tokens, index) => { + openRender: (tokens, index, _options, env) => { const title = tokens[index].info .trimStart() // "chartjs" length @@ -18,21 +48,40 @@ export const chartjs: PluginSimple = (md) => { .trim() let config = '{}' - let configType = '' + let isJavaScript = false + let isInAllowList = false + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const filePathRelative: string = env?.filePathRelative ?? '' for (let i = index; i < tokens.length; i++) { const { type, content, info } = tokens[i] - if (type === 'container_chartjs_close') break + if (type === 'container_chartjs_close') { + if (isJavaScript && !isInAllowList) { + // eslint-disable-next-line no-console + console.warn( + `\ +${colors.magenta('chartjs')}: JavaScript in echarts block is found in ${colors.cyan(filePathRelative)}, ${colors.red("it's ignored for security reasons")}. +To enable the chart, you must manually add it to allowlist, see https://vuepress.vuejs.org/plugin/markdown/markdown-charts/echarts.html for details. +`, + ) + tokens[i].hidden = true + } + break + } if (!content) continue if (type === 'fence') if (info === 'json') { config = encodeData(content) - configType = 'json' } else if (info === 'js' || info === 'javascript') { config = encodeData(content) - configType = 'js' + isJavaScript = true + + if (allowScripts && (allowAll || allowList.has(filePathRelative))) { + isInAllowList = true + } } // Set to an unknown token type @@ -41,10 +90,14 @@ export const chartjs: PluginSimple = (md) => { tokens[i].hidden = true } + if (isJavaScript && !isInAllowList) { + return '' + } + return `` + }${isJavaScript ? ' type="js"' : ''}>` }, - closeRender: () => ``, + closeRender: (tokens, index) => (tokens[index].hidden ? '' : ''), }) } diff --git a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts index 2cccde4659..562e1b6196 100644 --- a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts +++ b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts @@ -1,7 +1,8 @@ import { container } from '@mdit/plugin-container' import { encodeData } from '@vuepress/helper' -import type { PluginSimple } from 'markdown-it' +import type { PluginWithOptions } from 'markdown-it' import type Token from 'markdown-it/lib/token.mjs' +import { colors } from 'vuepress/utils' const echartsRender = (tokens: Token[], index: number): string => { const { content, info } = tokens[index] @@ -12,12 +13,42 @@ const echartsRender = (tokens: Token[], index: number): string => { }>` } +export interface EChartsPluginOptions { + /** + * Allow executing custom scripts inside ECharts blocks. + * + * 允许在 ECharts 块内执行自定义脚本。 + * + * @default false + */ + allowScripts?: boolean + + /** + * Allow all scripts to be executed inside Echarts blocks. + * 允许在 Echarts 块内执行所有脚本。 + * + * @default false + */ + allowAll?: boolean + + /** + * List of files allowed to execute scripts inside Echarts blocks. + * 允许在 Echarts 块内执行脚本的文件列表。 + */ + allowList?: Set +} + /** * ECharts markdown-it plugin * * ECharts markdown-it 插件 */ -export const echarts: PluginSimple = (md) => { +export const echarts: PluginWithOptions = ( + md, + options, +) => { + const { allowScripts, allowAll, allowList = new Set() } = options! + // Handle ```echarts blocks const { fence } = md.renderer.rules @@ -36,7 +67,7 @@ export const echarts: PluginSimple = (md) => { container(md, { name: 'echarts', - openRender: (tokens, index) => { + openRender: (tokens, index, _options, env) => { const title = tokens[index].info .trimStart() // 'echarts' length @@ -45,11 +76,27 @@ export const echarts: PluginSimple = (md) => { let config = '{}' let isJavaScript = false + let isInAllowList = false + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const filePathRelative: string = env?.filePathRelative ?? '' for (let i = index; i < tokens.length; i++) { const { type, content, info } = tokens[i] - if (type === 'container_echarts_close') break + if (type === 'container_echarts_close') { + if (isJavaScript && !isInAllowList) { + // eslint-disable-next-line no-console + console.warn( + `\ +${colors.magenta('[echarts]')}: JavaScript in echarts block is found in ${colors.cyan(filePathRelative)}, ${colors.red("it's ignored for security reasons")}. +To enable the chart, you must manually add it to allowlist, see https://vuepress.vuejs.org/plugin/markdown/markdown-charts/echarts.html for details. +`, + ) + tokens[i].hidden = true + } + break + } if (!content) continue if (type === 'fence') @@ -58,6 +105,10 @@ export const echarts: PluginSimple = (md) => { } else if (info === 'js' || info === 'javascript') { config = content isJavaScript = true + + if (allowScripts && (allowAll || allowList.has(filePathRelative))) { + isInAllowList = true + } } // Set to an unknown token type @@ -66,10 +117,14 @@ export const echarts: PluginSimple = (md) => { tokens[i].hidden = true } + if (isJavaScript && !isInAllowList) { + return '' + } + return `` }, - closeRender: () => ``, + closeRender: (tokens, index) => (tokens[index].hidden ? '' : ''), }) } diff --git a/plugins/markdown/plugin-markdown-chart/src/node/markdownChartPlugin.ts b/plugins/markdown/plugin-markdown-chart/src/node/markdownChartPlugin.ts index 825c1ec8c3..bb4d185850 100644 --- a/plugins/markdown/plugin-markdown-chart/src/node/markdownChartPlugin.ts +++ b/plugins/markdown/plugin-markdown-chart/src/node/markdownChartPlugin.ts @@ -77,8 +77,32 @@ export const markdownChartPlugin = name: PLUGIN_NAME, extendsMarkdown: (md) => { - if (status.chartjs) md.use(chartjs) - if (status.echarts) md.use(echarts) + const { + DANGEROUS_ALLOW_SCRIPT_EXECUTION, + DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST, + } = options + + const scriptOptions = { + allowScripts: DANGEROUS_ALLOW_SCRIPT_EXECUTION ?? false, + allowAll: DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST === '*', + allowList: new Set( + isArray(DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST) + ? DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST.map((file) => { + const result = file + // normalize `\` to `/` on Windows + .replace(/\\/g, '/') + // remove any leading slash + .replace(/^\//, '') + + // ensure markdown extension + return result.endsWith('.md') ? result : `${result}.md` + }) + : [], + ), + } + + if (status.chartjs) md.use(chartjs, scriptOptions) + if (status.echarts) md.use(echarts, scriptOptions) if (status.flowchart) md.use(flowchart) if (isArray(options.plantuml)) md.use(plantuml, options.plantuml) else if (options.plantuml) md.use(plantuml) diff --git a/plugins/markdown/plugin-markdown-chart/src/node/options.ts b/plugins/markdown/plugin-markdown-chart/src/node/options.ts index 2ff95362f0..e97193b8b7 100644 --- a/plugins/markdown/plugin-markdown-chart/src/node/options.ts +++ b/plugins/markdown/plugin-markdown-chart/src/node/options.ts @@ -54,4 +54,28 @@ export interface MarkdownChartPluginOptions { * @default false */ plantuml?: MarkdownItPlantumlOptions[] | boolean + + /** + * Allow executing custom scripts inside chart blocks. + * + * 允许在图表代码块中执行自定义脚本。 + * + * # Caution! Enabling this may introduce XSS or remote code risks. + * + * # 注意!启用可能带来 XSS 或远程代码风险。 + * + * @default false + */ + DANGEROUS_ALLOW_SCRIPT_EXECUTION?: boolean + + /** + * Allowlist of source files permitted to run chart scripts. + * + * 允许执行图表脚本的源文件列表(允许清单)。 + * + * Only effective when `DANGEROUS_ALLOW_SCRIPT_EXECUTION` is set; paths must match real files. + * + * 仅在 `DANGEROUS_ALLOW_SCRIPT_EXECUTION` 被设置后生效;路径需与实际文件匹配。 + */ + DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST?: string[] | '*' } From ec00835573667061ae471dc6bf3fca04c905e5da Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Mon, 24 Nov 2025 16:35:30 +0800 Subject: [PATCH 2/5] docs: improve docs wording --- docs/plugins/markdown/markdown-chart/README.md | 2 +- docs/zh/plugins/markdown/markdown-chart/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/markdown/markdown-chart/README.md b/docs/plugins/markdown/markdown-chart/README.md index 9f3389cfcd..5a5b64dae8 100644 --- a/docs/plugins/markdown/markdown-chart/README.md +++ b/docs/plugins/markdown/markdown-chart/README.md @@ -135,4 +135,4 @@ export default { - Type: `string[] | '*'` - Default: `[]` -- Details: A list of allowed script sources when script execution is enabled. Use `'*'` to allow all sources. +- Details: A list of sources files that script execution is enabled. Use `'*'` to allow all source files. diff --git a/docs/zh/plugins/markdown/markdown-chart/README.md b/docs/zh/plugins/markdown/markdown-chart/README.md index 5b15ba1cda..b3b3eb2407 100644 --- a/docs/zh/plugins/markdown/markdown-chart/README.md +++ b/docs/zh/plugins/markdown/markdown-chart/README.md @@ -135,4 +135,4 @@ export default { - 类型:`string[] | '*'` - 默认:`[]` -- 详情:当启用脚本执行时,允许的脚本源列表。使用 `'*'` 允许所有源。 +- 详情:当启用脚本执行时,允许脚本运行的源文件列表。使用 `'*'` 允许所有源文件。 From defd113281bb235c1001381601949f7ccb9b3507 Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Mon, 24 Nov 2025 17:05:48 +0800 Subject: [PATCH 3/5] chore: tweaks --- .../plugins/markdown/markdown-chart/README.md | 2 +- .../plugins/markdown/markdown-chart/README.md | 2 +- .../src/client/components/ECharts.ts | 16 ++--- .../src/node/markdown-it-plugins/chartjs.ts | 10 +-- .../src/node/markdown-it-plugins/echarts.ts | 14 ++--- .../node/__snapshots__/chartjs.spec.ts.snap | 4 +- .../tests/node/chartjs.spec.ts | 63 ++++++++++++++++++- .../tests/node/echarts.spec.ts | 41 +++++++++++- 8 files changed, 124 insertions(+), 28 deletions(-) diff --git a/docs/plugins/markdown/markdown-chart/README.md b/docs/plugins/markdown/markdown-chart/README.md index 5a5b64dae8..dd7c0124c7 100644 --- a/docs/plugins/markdown/markdown-chart/README.md +++ b/docs/plugins/markdown/markdown-chart/README.md @@ -135,4 +135,4 @@ export default { - Type: `string[] | '*'` - Default: `[]` -- Details: A list of sources files that script execution is enabled. Use `'*'` to allow all source files. +- Details: Only effective when `DANGEROUS_ALLOW_SCRIPT_EXECUTION` is enabled. A list of file paths allowed to execute chart scripts. Use `'*'` to allow all files. diff --git a/docs/zh/plugins/markdown/markdown-chart/README.md b/docs/zh/plugins/markdown/markdown-chart/README.md index b3b3eb2407..511273044f 100644 --- a/docs/zh/plugins/markdown/markdown-chart/README.md +++ b/docs/zh/plugins/markdown/markdown-chart/README.md @@ -135,4 +135,4 @@ export default { - 类型:`string[] | '*'` - 默认:`[]` -- 详情:当启用脚本执行时,允许脚本运行的源文件列表。使用 `'*'` 允许所有源文件。 +- 详情:当启用脚本执行时,允许执行图表脚本的文件路径列表。使用 `'*'` 允许所有文件。 diff --git a/plugins/markdown/plugin-markdown-chart/src/client/components/ECharts.ts b/plugins/markdown/plugin-markdown-chart/src/client/components/ECharts.ts index d1365e19d2..5ad8431eb9 100644 --- a/plugins/markdown/plugin-markdown-chart/src/client/components/ECharts.ts +++ b/plugins/markdown/plugin-markdown-chart/src/client/components/ECharts.ts @@ -1,7 +1,7 @@ import { LoadingIcon, decodeData } from '@vuepress/helper/client' import { useDebounceFn, useEventListener } from '@vueuse/core' import type { EChartsOption, EChartsType } from 'echarts' -import type * as Echarts from 'echarts' +import type * as ECharts from 'echarts' import type { PropType, VNode } from 'vue' import { defineComponent, @@ -30,7 +30,7 @@ const AsyncFunction = (async (): Promise => {}).constructor const parseEChartsConfig = ( config: string, type: 'js' | 'json', - echarts: typeof Echarts, + echarts: typeof ECharts, instance: EChartsType, ): Promise => { if (type === 'js') { @@ -102,12 +102,12 @@ export default defineComponent({ }, 100), ) - const destroyEcharts = (): void => { + const destroyECharts = (): void => { instance?.dispose() instance = null } - const renderEcharts = async (): Promise => { + const renderECharts = async (): Promise => { if (__VUEPRESS_SSR__) return const echarts = await import(/* webpackChunkName: "echarts" */ 'echarts') @@ -129,7 +129,7 @@ export default defineComponent({ onContentUpdated(async (reason) => { if (reason === 'mounted') { - await renderEcharts() + await renderECharts() loaded.value = true } }) @@ -141,15 +141,15 @@ export default defineComponent({ watch( () => props.config, async () => { - destroyEcharts() + destroyECharts() await nextTick() - await renderEcharts() + await renderECharts() }, { flush: 'post' }, ) }) - onUnmounted(destroyEcharts) + onUnmounted(destroyECharts) return (): (VNode | null)[] => [ props.title diff --git a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts index a528f59a6b..ff4c718e1a 100644 --- a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts +++ b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts @@ -36,7 +36,7 @@ export const chartjs: PluginWithOptions = ( md, options, ) => { - const { allowScripts, allowAll, allowList = new Set() } = options! + const { allowScripts, allowAll, allowList = new Set() } = options ?? {} container(md, { name: 'chartjs', @@ -62,8 +62,8 @@ export const chartjs: PluginWithOptions = ( // eslint-disable-next-line no-console console.warn( `\ -${colors.magenta('chartjs')}: JavaScript in echarts block is found in ${colors.cyan(filePathRelative)}, ${colors.red("it's ignored for security reasons")}. -To enable the chart, you must manually add it to allowlist, see https://vuepress.vuejs.org/plugin/markdown/markdown-charts/echarts.html for details. +${colors.magenta('chartjs')}: JavaScript in Chart.js block is found in ${colors.cyan(filePathRelative)}, ${colors.red('it is ignored for security reasons')}. +To enable the chart, you must manually add it to allowlist, see https://vuepress.vuejs.org/plugin/markdown/markdown-charts/chartjs.html for details. `, ) tokens[i].hidden = true @@ -94,8 +94,8 @@ To enable the chart, you must manually add it to allowlist, see https://vuepress return '' } - return `` }, closeRender: (tokens, index) => (tokens[index].hidden ? '' : ''), diff --git a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts index 562e1b6196..1569ccdecf 100644 --- a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts +++ b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts @@ -24,16 +24,16 @@ export interface EChartsPluginOptions { allowScripts?: boolean /** - * Allow all scripts to be executed inside Echarts blocks. - * 允许在 Echarts 块内执行所有脚本。 + * Allow all scripts to be executed inside ECharts blocks. + * 允许在 ECharts 块内执行所有脚本。 * * @default false */ allowAll?: boolean /** - * List of files allowed to execute scripts inside Echarts blocks. - * 允许在 Echarts 块内执行脚本的文件列表。 + * List of files allowed to execute scripts inside ECharts blocks. + * 允许在 ECharts 块内执行脚本的文件列表。 */ allowList?: Set } @@ -47,7 +47,7 @@ export const echarts: PluginWithOptions = ( md, options, ) => { - const { allowScripts, allowAll, allowList = new Set() } = options! + const { allowScripts, allowAll, allowList = new Set() } = options ?? {} // Handle ```echarts blocks const { fence } = md.renderer.rules @@ -89,8 +89,8 @@ export const echarts: PluginWithOptions = ( // eslint-disable-next-line no-console console.warn( `\ -${colors.magenta('[echarts]')}: JavaScript in echarts block is found in ${colors.cyan(filePathRelative)}, ${colors.red("it's ignored for security reasons")}. -To enable the chart, you must manually add it to allowlist, see https://vuepress.vuejs.org/plugin/markdown/markdown-charts/echarts.html for details. +${colors.magenta('[echarts]')}: JavaScript in echarts block is found in ${colors.cyan(filePathRelative)}, ${colors.red('it is ignored for security reasons')}. +To enable the chart, you must manually add it to allowlist, see https://vuepress.vuejs.org/plugin/markdown/markdown-chart/echarts.html for details. `, ) tokens[i].hidden = true diff --git a/plugins/markdown/plugin-markdown-chart/tests/node/__snapshots__/chartjs.spec.ts.snap b/plugins/markdown/plugin-markdown-chart/tests/node/__snapshots__/chartjs.spec.ts.snap index 794e79327d..9307732f0f 100644 --- a/plugins/markdown/plugin-markdown-chart/tests/node/__snapshots__/chartjs.spec.ts.snap +++ b/plugins/markdown/plugin-markdown-chart/tests/node/__snapshots__/chartjs.spec.ts.snap @@ -8,8 +8,8 @@ exports[`chartjs > Should not break markdown fence 1`] = ` exports[`chartjs > Should resolve chart info with javascript block 1`] = `""`; -exports[`chartjs > Should resolve chart with empty title and body 1`] = `""`; +exports[`chartjs > Should resolve chart with empty title and body 1`] = `""`; exports[`chartjs > Should resolve chartjs info with js block 1`] = `""`; -exports[`chartjs > Should resolve chartjs info with json block 1`] = `""`; +exports[`chartjs > Should resolve chartjs info with json block 1`] = `""`; diff --git a/plugins/markdown/plugin-markdown-chart/tests/node/chartjs.spec.ts b/plugins/markdown/plugin-markdown-chart/tests/node/chartjs.spec.ts index bf1f63870a..2214f3030a 100644 --- a/plugins/markdown/plugin-markdown-chart/tests/node/chartjs.spec.ts +++ b/plugins/markdown/plugin-markdown-chart/tests/node/chartjs.spec.ts @@ -4,7 +4,10 @@ import { describe, expect, it } from 'vitest' import { chartjs } from '../../src/node/markdown-it-plugins/chartjs.js' describe('chartjs', () => { - const markdownIt = MarkdownIt({ linkify: true }).use(chartjs) + const markdownIt = MarkdownIt({ linkify: true }).use(chartjs, { + allowScripts: true, + allowAll: true, + }) it('Should resolve chartjs info with json block', () => { const result = markdownIt.render( @@ -57,7 +60,7 @@ describe('chartjs', () => { expect(result).toMatch(/<\/ChartJS>/) expect(result).toContain(`title="${encodeURIComponent('A bar chart')}"`) - expect(result).toContain('type="json"') + expect(result).not.toContain('type="') expect(result).toMatchSnapshot() }) @@ -183,7 +186,7 @@ const config = { expect(result).toMatch(/<\/ChartJS>/) expect(result).not.toContain('title="') - expect(result).toContain('type=""') + expect(result).not.toContain('type="') expect(result).toMatchSnapshot() }) @@ -200,4 +203,58 @@ const a = 1; expect(result).toMatch(/[\s\S]*<\/pre>/) expect(result).toMatchSnapshot() }) + + it('Should remove unsafe script block by default', () => { + const md = MarkdownIt({ linkify: true }).use(chartjs) + + const result = md.render( + ` +::: chartjs A bar chart + +\`\`\`js +const config = { + type: "bar", + data: { + labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"], + datasets: [ + { + label: "# of Votes", + data: [12, 19, 3, 5, 2, 3], + backgroundColor: [ + "rgba(255, 99, 132, 0.2)", + "rgba(54, 162, 235, 0.2)", + "rgba(255, 206, 86, 0.2)", + "rgba(75, 192, 192, 0.2)", + "rgba(153, 102, 255, 0.2)", + "rgba(255, 159, 64, 0.2)", + ], + borderColor: [ + "rgba(255, 99, 132, 1)", + "rgba(54, 162, 235, 1)", + "rgba(255, 206, 86, 1)", + "rgba(75, 192, 192, 1)", + "rgba(153, 102, 255, 1)", + "rgba(255, 159, 64, 1)", + ], + borderWidth: 1, + }, + ], + }, + options: { + scales: { + y: { + beginAtZero: true, + }, + }, + }, +}; +\`\`\` + +::: +`, + {}, + ) + + expect(result).toMatch('') + }) }) diff --git a/plugins/markdown/plugin-markdown-chart/tests/node/echarts.spec.ts b/plugins/markdown/plugin-markdown-chart/tests/node/echarts.spec.ts index edd849bd96..fd11d7eddd 100644 --- a/plugins/markdown/plugin-markdown-chart/tests/node/echarts.spec.ts +++ b/plugins/markdown/plugin-markdown-chart/tests/node/echarts.spec.ts @@ -4,7 +4,10 @@ import { describe, expect, it } from 'vitest' import { echarts } from '../../src/node/markdown-it-plugins/echarts.js' describe('echarts', () => { - const markdownIt = MarkdownIt({ linkify: true }).use(echarts) + const markdownIt = MarkdownIt({ linkify: true }).use(echarts, { + allowScripts: true, + allowAll: true, + }) it('Should resolve echarts container with json block', () => { const result = markdownIt.render( @@ -182,4 +185,40 @@ const a = 1; expect(result).toMatch(/[\s\S]*<\/pre>/) expect(result).toMatchSnapshot() }) + + it('Should remove unsafe script block by default', () => { + const md = MarkdownIt({ linkify: true }).use(echarts) + + const result = md.render( + ` +::: echarts A bar chart + +\`\`\`js +const option = { + xAxis: { + type: "category", + data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + }, + yAxis: { + type: "value", + }, + series: [ + { + data: [150, 230, 224, 218, 135, 147, 260], + type: "line", + }, + ], + tooltip: { + trigger: "axis", + }, +}; +\`\`\` + +::: +`, + {}, + ) + + expect(result).toMatch('') + }) }) From eb83b23d605af0135d9a054dfbb2a089a656d9fe Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Mon, 24 Nov 2025 17:10:16 +0800 Subject: [PATCH 4/5] chore: tweaks --- .../src/node/markdown-it-plugins/chartjs.ts | 7 ++++++- .../src/node/markdown-it-plugins/echarts.ts | 4 ++++ plugins/markdown/plugin-markdown-chart/src/node/options.ts | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts index ff4c718e1a..4d85f00673 100644 --- a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts +++ b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/chartjs.ts @@ -6,6 +6,7 @@ import { colors } from 'vuepress/utils' export interface ChartJSPluginOptions { /** * Allow executing custom scripts inside Chart.js blocks. + * * 允许在 Chart.js 块内执行自定义脚本。 * * @default false @@ -14,6 +15,7 @@ export interface ChartJSPluginOptions { /** * Allow all scripts to be executed inside Chart.js blocks. + * * 允许在 Chart.js 块内执行所有脚本。 * * @default false @@ -22,9 +24,12 @@ export interface ChartJSPluginOptions { /** * List of files allowed to execute scripts inside Chart.js blocks. + * * 允许在 Chart.js 块内执行脚本的文件列表。 + * + * @default new Set() */ - allowList: Set + allowList?: Set } /** diff --git a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts index 1569ccdecf..83c615eb84 100644 --- a/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts +++ b/plugins/markdown/plugin-markdown-chart/src/node/markdown-it-plugins/echarts.ts @@ -25,6 +25,7 @@ export interface EChartsPluginOptions { /** * Allow all scripts to be executed inside ECharts blocks. + * * 允许在 ECharts 块内执行所有脚本。 * * @default false @@ -33,7 +34,10 @@ export interface EChartsPluginOptions { /** * List of files allowed to execute scripts inside ECharts blocks. + * * 允许在 ECharts 块内执行脚本的文件列表。 + * + * @default new Set() */ allowList?: Set } diff --git a/plugins/markdown/plugin-markdown-chart/src/node/options.ts b/plugins/markdown/plugin-markdown-chart/src/node/options.ts index e97193b8b7..87f4e821ed 100644 --- a/plugins/markdown/plugin-markdown-chart/src/node/options.ts +++ b/plugins/markdown/plugin-markdown-chart/src/node/options.ts @@ -73,7 +73,7 @@ export interface MarkdownChartPluginOptions { * * 允许执行图表脚本的源文件列表(允许清单)。 * - * Only effective when `DANGEROUS_ALLOW_SCRIPT_EXECUTION` is set; paths must match real files. + * @description Only effective when `DANGEROUS_ALLOW_SCRIPT_EXECUTION` is set; paths must match real files. * * 仅在 `DANGEROUS_ALLOW_SCRIPT_EXECUTION` 被设置后生效;路径需与实际文件匹配。 */ From 7599f9a715e4f8bee8dcf819f31a8def96036241 Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Mon, 24 Nov 2025 17:16:42 +0800 Subject: [PATCH 5/5] docs: update docs --- docs/plugins/markdown/markdown-chart/chartjs.md | 8 +++++++- docs/plugins/markdown/markdown-chart/echarts.md | 10 ++++++++-- docs/zh/plugins/markdown/markdown-chart/chartjs.md | 8 +++++++- docs/zh/plugins/markdown/markdown-chart/echarts.md | 10 ++++++++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/docs/plugins/markdown/markdown-chart/chartjs.md b/docs/plugins/markdown/markdown-chart/chartjs.md index 233dc80ee9..9d5d6e5c97 100644 --- a/docs/plugins/markdown/markdown-chart/chartjs.md +++ b/docs/plugins/markdown/markdown-chart/chartjs.md @@ -65,7 +65,13 @@ export default { ::: ```` -Both `js` and `javascript` code blocks are also supported. For these, you should assign your export object to `module.exports`. +You should use `json` code block to provide your Chart.js configuration whenever possible, however for dynamic data generation, you can also use script blocks. Both `js` and `javascript` code blocks are also supported. You should assign your export object to `module.exports`. + +::: warning + +For security reasons, you need to manually allow script blocks in certain files. Set `DANGEROUS_ALLOW_SCRIPT_EXECUTION: true` and `DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST: ['your/file/path.md']` in plugin options. + +::: ## Demo diff --git a/docs/plugins/markdown/markdown-chart/echarts.md b/docs/plugins/markdown/markdown-chart/echarts.md index 84d6b7f1e6..4a91c5f730 100644 --- a/docs/plugins/markdown/markdown-chart/echarts.md +++ b/docs/plugins/markdown/markdown-chart/echarts.md @@ -71,9 +71,15 @@ If you can generate your chart data easily, you can just provide echarts config ### With Scripts -If you need to use script to get the data, you can use `js` or `javascript` code block. +You should use `json` code block to provide your ECharts configuration whenever possible, however for dynamic data generation, you can also use script blocks. -We will expose the echarts lib as `echarts` and the instance as `myChart` in the script, and you are expected to assign the echarts option object to `option` variable. Also, you can assign `width` and `height` variable to set the chart size. +Both `js` or `javascript` code block are supported. We will expose the echarts lib as `echarts` and the instance as `myChart` in the script, and you are expected to assign the echarts option object to `option` variable. Also, you can assign `width` and `height` variable to set the chart size. + +::: warning + +For security reasons, you need to manually allow script blocks in certain files. Set `DANGEROUS_ALLOW_SCRIPT_EXECUTION: true` and `DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST: ['your/file/path.md']` in plugin options. + +::: ````md ::: echarts Title diff --git a/docs/zh/plugins/markdown/markdown-chart/chartjs.md b/docs/zh/plugins/markdown/markdown-chart/chartjs.md index 6a1cd342c1..b07836b6f4 100644 --- a/docs/zh/plugins/markdown/markdown-chart/chartjs.md +++ b/docs/zh/plugins/markdown/markdown-chart/chartjs.md @@ -65,7 +65,13 @@ export default { ::: ```` -同时支持 `js` 和 `javascript` 代码块。对于这些,你应该将导出对象赋值给 `module.exports`。 +你应该尽可能使用 `json` 代码块来提供你的图表数据配置,但如果需要动态生成数据,你也可以使用脚本块。`js` 或 `javascript` 代码块均受支持。你应当将导出的对象赋值给 `module.exports`。 + +::: warning + +出于安全考虑,你需要手动允许特定文件中的脚本块。请在插件选项中设置 `DANGEROUS_ALLOW_SCRIPT_EXECUTION: true` 和 `DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST: ['your/file/path.md']`。 + +::: ## 案例 diff --git a/docs/zh/plugins/markdown/markdown-chart/echarts.md b/docs/zh/plugins/markdown/markdown-chart/echarts.md index 4f9c3c3451..02bb42850c 100644 --- a/docs/zh/plugins/markdown/markdown-chart/echarts.md +++ b/docs/zh/plugins/markdown/markdown-chart/echarts.md @@ -73,9 +73,15 @@ export default { ### 使用脚本 -如果你需要通过脚本来获取数据,你可以使用 `js` 和 `javascript` 的代码块。 +你应该尽可能使用 `json` 代码块来提供你的 ECharts 配置,但如果需要动态生成数据,你也可以使用脚本块。 -我们将通过 `echarts` 变量暴露 ECharts 库以及 `myChart` 暴露 ECharts 实例,并且你应该将 Echart 配置赋值给 `option` 变量。同时,你也可以赋值 `width` 和 `height` 来设置图表大小。 +`js` 或 `javascript` 代码块均受支持。在脚本中,我们会将 echarts 库作为 `echarts`,实例作为 `myChart` 暴露给你,你需要将 echarts 配置对象赋值给 `option` 变量。同时,你也可以通过设置 `width` 和 `height` 变量来设置图表大小。 + +::: warning + +出于安全考虑,你需要手动允许特定文件中的脚本块。请在插件选项中设置 `DANGEROUS_ALLOW_SCRIPT_EXECUTION: true` 和 `DANGEROUS_SCRIPT_EXECUTION_ALLOWLIST: ['your/file/path.md']`。 + +::: ````md ::: echarts Title