From 822d71a0cc925c6c92c4149d92f61c602313f550 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 15 Jul 2026 15:03:16 +0800 Subject: [PATCH 1/3] fix: prevent vtable-sheet formula code execution --- .../__tests__/basic-formula-test.test.ts | 33 +++++ .../src/formula/formula-engine.ts | 133 +++++++++++++++++- 2 files changed, 160 insertions(+), 6 deletions(-) diff --git a/packages/vtable-sheet/__tests__/basic-formula-test.test.ts b/packages/vtable-sheet/__tests__/basic-formula-test.test.ts index 48541809b5..d790f25c58 100644 --- a/packages/vtable-sheet/__tests__/basic-formula-test.test.ts +++ b/packages/vtable-sheet/__tests__/basic-formula-test.test.ts @@ -1,4 +1,5 @@ import { FormulaManager } from '../src/managers/formula-manager'; +import { FormulaEngine } from '../src/formula/formula-engine'; // Mock VTableSheet for testing const mockVTableSheet = { @@ -92,4 +93,36 @@ describe('Basic Formula Functionality', () => { expect(formulas['A1']).toBe('=SUM(A1:A2)'); expect(formulas['A2']).toBe('=A1*2'); }); + + test('should evaluate arithmetic formulas without executing cell content as code', () => { + const engine = new FormulaEngine(); + const marker = '__vtableSheetRceExecuted'; + const payload = `0,globalThis.${marker}=1,0`; + + delete (globalThis as any)[marker]; + engine.addSheet('Sheet1', [['', '']]); + engine.setActiveSheet('Sheet1'); + + engine.updateSheetData('Sheet1', [[payload], ['=A1+1']]); + const batchResult = engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 0 }); + expect(batchResult.error).toBeTruthy(); + expect((globalThis as any)[marker]).toBeUndefined(); + + engine.setCellContent({ sheet: 'Sheet1', row: 0, col: 0 }, payload); + engine.setCellContent({ sheet: 'Sheet1', row: 0, col: 1 }, '=A1+1'); + const singleResult = engine.getCellValue({ sheet: 'Sheet1', row: 0, col: 1 }); + expect(singleResult.error).toBeTruthy(); + expect((globalThis as any)[marker]).toBeUndefined(); + }); + + test('should keep numeric arithmetic, precedence and functions working', () => { + const engine = new FormulaEngine(); + engine.addSheet('Sheet1', [[2], ['=A1+1'], ['=SUM(A1:A2)+3'], ['=1+2*3'], ['=-(1+2)*3']]); + engine.setActiveSheet('Sheet1'); + + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 0 })).toEqual({ value: 3, error: undefined }); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 2, col: 0 })).toEqual({ value: 8, error: undefined }); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 3, col: 0 })).toEqual({ value: 7, error: undefined }); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 4, col: 0 })).toEqual({ value: -9, error: undefined }); + }); }); diff --git a/packages/vtable-sheet/src/formula/formula-engine.ts b/packages/vtable-sheet/src/formula/formula-engine.ts index 7633ba4a51..52e17f9b97 100644 --- a/packages/vtable-sheet/src/formula/formula-engine.ts +++ b/packages/vtable-sheet/src/formula/formula-engine.ts @@ -1502,26 +1502,147 @@ export class FormulaEngine { if (funcResult.error) { return { value: null, error: `Error in function ${funcCall}: ${funcResult.error}` }; } + const numericFuncValue = this.toArithmeticNumber(funcResult.value); + if (numericFuncValue.error) { + return { value: null, error: numericFuncValue.error }; + } processedExpr = - processedExpr.slice(0, funcSpan.start) + String(funcResult.value) + processedExpr.slice(funcSpan.end + 1); + processedExpr.slice(0, funcSpan.start) + + String(numericFuncValue.value) + + processedExpr.slice(funcSpan.end + 1); } // 3. 处理剩余的单元格引用(包括带sheet前缀的引用,支持带引号的sheet名称) const cellRefs = processedExpr.match(/('[^']+'!)?([A-Za-z0-9_\s一-龥]+!)?[A-Z]+[0-9]+/g) || []; for (const cellRef of cellRefs) { const value = this.getCellValueByA1(cellRef); - processedExpr = processedExpr.replace(cellRef, String(value)); + const numericValue = this.toArithmeticNumber(value); + if (numericValue.error) { + return { value: null, error: numericValue.error }; + } + processedExpr = processedExpr.replace(cellRef, String(numericValue.value)); } - // 4. 计算最终的算术表达式 - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const result = Function('"use strict"; return (' + processedExpr + ')')(); - return { value: result, error: undefined }; + // 4. 使用白名单算术解析器计算,禁止通过 Function/eval 执行用户可控表达式 + return this.evaluateBasicArithmetic(processedExpr); } catch (error) { return { value: null, error: 'Basic arithmetic evaluation failed' }; } } + private toArithmeticNumber(value: unknown): { value: number; error?: string } { + if (value === null || value === undefined || value === '') { + return { value: 0, error: undefined }; + } + + const num = Number(value); + if (!Number.isFinite(num)) { + return { value: 0, error: 'Arithmetic operands must be numeric' }; + } + + return { value: num, error: undefined }; + } + + private evaluateBasicArithmetic(expr: string): { value: unknown; error?: string } { + try { + let index = 0; + + const skipWhitespace = () => { + while (index < expr.length && /\s/.test(expr[index])) { + index++; + } + }; + + const parseNumber = (): number | null => { + skipWhitespace(); + const match = expr.slice(index).match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?/i); + if (!match) { + return null; + } + index += match[0].length; + return Number(match[0]); + }; + + const parseFactor = (): number => { + skipWhitespace(); + + if (expr[index] === '+') { + index++; + return parseFactor(); + } + if (expr[index] === '-') { + index++; + return -parseFactor(); + } + if (expr[index] === '(') { + index++; + const value = parseAdditive(); + skipWhitespace(); + if (expr[index] !== ')') { + throw new Error('Missing closing parenthesis'); + } + index++; + return value; + } + + const value = parseNumber(); + if (value === null || !Number.isFinite(value)) { + throw new Error('Invalid arithmetic token'); + } + return value; + }; + + const parseMultiplicative = (): number => { + let value = parseFactor(); + while (true) { + skipWhitespace(); + const operator = expr[index]; + if (operator !== '*' && operator !== '/') { + break; + } + index++; + const right = parseFactor(); + if (operator === '*') { + value *= right; + } else { + value /= right; + } + } + return value; + }; + + const parseAdditive = (): number => { + let value = parseMultiplicative(); + while (true) { + skipWhitespace(); + const operator = expr[index]; + if (operator !== '+' && operator !== '-') { + break; + } + index++; + const right = parseMultiplicative(); + if (operator === '+') { + value += right; + } else { + value -= right; + } + } + return value; + }; + + const result = parseAdditive(); + skipWhitespace(); + + if (index !== expr.length || !Number.isFinite(result)) { + return { value: null, error: 'Basic arithmetic evaluation failed' }; + } + + return { value: result, error: undefined }; + } catch { + return { value: null, error: 'Basic arithmetic evaluation failed' }; + } + } + private findInnermostFunctionCallSpan(expr: string): { start: number; end: number } | null { type Frame = { funcStart: number | null }; const stack: Frame[] = []; From 6d5e75ab8b41ef095b4d597318637d08293303f6 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 15 Jul 2026 15:12:28 +0800 Subject: [PATCH 2/3] chore: add vtable-sheet change log --- .../fix-formula-code-execution_2026-07-15-10-53.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vtable-sheet/fix-formula-code-execution_2026-07-15-10-53.json diff --git a/common/changes/@visactor/vtable-sheet/fix-formula-code-execution_2026-07-15-10-53.json b/common/changes/@visactor/vtable-sheet/fix-formula-code-execution_2026-07-15-10-53.json new file mode 100644 index 0000000000..edb511304b --- /dev/null +++ b/common/changes/@visactor/vtable-sheet/fix-formula-code-execution_2026-07-15-10-53.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-sheet", + "comment": "fix: prevent formula arithmetic evaluation from executing user-controlled cell content", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-sheet", + "email": "892739385@qq.com" +} From 1cfc249f9a1a0b2fb8780d9b42e556323d03b706 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 15 Jul 2026 16:01:18 +0800 Subject: [PATCH 3/3] docs: clarify vtable-sheet formula arithmetic --- docs/assets/guide/en/sheet/formula.md | 3 ++- docs/assets/guide/zh/sheet/formula.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/assets/guide/en/sheet/formula.md b/docs/assets/guide/en/sheet/formula.md index 1919ba20f8..de0d9659e2 100644 --- a/docs/assets/guide/en/sheet/formula.md +++ b/docs/assets/guide/en/sheet/formula.md @@ -164,9 +164,10 @@ Supports common formula error handling: - Supports nested functions, such as `=IF(SUM(A1:A5)>100, MAX(B1:B5), MIN(C1:C5))` - Cell addresses use A1 format, such as A1, B2, C3, etc. - Range references use a colon separator, such as A1:B10 +- Arithmetic expressions are evaluated as numeric calculations only. Cell values or function results used with `+`, `-`, `*`, or `/` must be convertible to finite numbers. Non-numeric content returns a formula error and is never executed as script or code. **The formula capabilities are not fully complete** **The formula linkage processing, formula and sorting, and dragging rows and columns have conflicts** **If other cell editors are manually configured, the formula capabilities will be disabled.** **The formula capabilities will be gradually improved in future updates.** -**Welcome to participate in the contribution of the formula capabilities.** \ No newline at end of file +**Welcome to participate in the contribution of the formula capabilities.** diff --git a/docs/assets/guide/zh/sheet/formula.md b/docs/assets/guide/zh/sheet/formula.md index 0aa77f2292..89696f84ba 100644 --- a/docs/assets/guide/zh/sheet/formula.md +++ b/docs/assets/guide/zh/sheet/formula.md @@ -161,8 +161,9 @@ VTableSheet 自身开发了 FormulaEngine 模块 作为核心的计算引擎: - 支持嵌套函数,如 `=IF(SUM(A1:A5)>100, MAX(B1:B5), MIN(C1:C5))` - 单元格地址使用A1格式,如A1, B2, C3等 - 区域引用使用冒号分隔,如A1:B10 +- 算术表达式只会按数值进行计算;参与 `+`、`-`、`*`、`/` 运算的单元格值或函数结果需要能转换为有效数字,非数值内容会返回公式错误,不会作为脚本或代码执行 **目前公式能力并不完善** **如公式的联动处理,公式和排序以及拖拽行列换位都有冲突** **手动配置了其他单元格编辑器editor后,公式能力会失效** -**后续会逐步完善这些功能,也欢迎有兴趣的开发者参与贡献** \ No newline at end of file +**后续会逐步完善这些功能,也欢迎有兴趣的开发者参与贡献**