Skip to content

Commit 3b3ac69

Browse files
Merge pull request #1174 from objectstack-ai/copilot/fix-evaluate-expression-error
fix(core): replace `new Function()` with CSP-safe recursive-descent expression parser
2 parents 5103725 + cfe304c commit 3b3ac69

5 files changed

Lines changed: 1347 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121

2222
### Fixed
2323

24+
- **CSP-safe expression evaluation** (`@object-ui/core`): Replaced `new Function()` in `ExpressionCache.compileExpression()` with a new `SafeExpressionParser` — a recursive-descent interpreter that evaluates expressions without any dynamic code execution. This fixes `Content Security Policy` violations (`'unsafe-eval' is not an allowed source of script`) that occurred when evaluating schema expressions like `${stage !== 'closed_won' && stage !== 'closed_lost'}` in enterprise deployments with strict CSP headers. The `SafeExpressionParser` supports the full ObjectUI expression language: all comparison and logical operators, ternary and nullish coalescing, arithmetic, unary operators (`!`, `-`, `+`, `typeof`), dot/bracket/optional-chaining member access, function calls (formula functions, method calls, `Math.*`), single-param arrow functions (for `.filter(u => u.isActive)`, `.map(x => x.name)` etc.), array literals, `new Date()` / `new RegExp()` constructors, and all literal types. The `ExpressionCache` public API is unchanged. 41 new tests added.
25+
2426
- **CI build errors** (`@object-ui/console`): Removed unused imports (`resolveI18nLabel` in `HomePage.tsx`, `Upload`/`FileText` in `QuickActions.tsx`) that caused TS6133 errors. Fixed `appConfig``appConfigs[0]` in `i18n-translations.test.ts` (TS2552). Extracted `customReportsConfig` from aggregated `sharedConfig` into a standalone export so the mock server kernel includes the `sales_performance_q1` report, fixing `ReportView.test.tsx` failures.
2527

2628
- **Charts groupBy value→label resolution** (`@object-ui/plugin-charts`): Chart X-axis labels now display human-readable labels instead of raw values. Select/picklist fields resolve value→label via field metadata options, lookup/master_detail fields batch-fetch referenced record names, and all other fields fall back to `humanizeLabel()` (snake_case → Title Case). Removed hardcoded `value.slice(0, 3)` truncation from `AdvancedChartImpl.tsx` XAxis tick formatters — desktop now shows full labels with angle rotation for long text, mobile truncates at 8 characters with "…".

packages/core/src/evaluator/ExpressionCache.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88

99
/**
1010
* @object-ui/core - Expression Cache
11-
*
11+
*
1212
* Caches compiled expressions to avoid re-parsing on every render.
1313
* Provides significant performance improvement for frequently evaluated expressions.
14-
*
14+
*
1515
* @module evaluator
1616
* @packageDocumentation
1717
*/
1818

19+
import { SafeExpressionParser } from './SafeExpressionParser.js';
20+
1921
/**
2022
* A compiled expression function that can be executed with context values
2123
*/
@@ -112,16 +114,28 @@ export class ExpressionCache {
112114
}
113115

114116
/**
115-
* Compile an expression into a function
117+
* Compile an expression into a CSP-safe callable function.
118+
*
119+
* Uses `SafeExpressionParser` — a recursive-descent interpreter — instead of
120+
* `new Function()` so that the expression engine works under strict
121+
* Content Security Policy headers that forbid `'unsafe-eval'`.
122+
*
123+
* A single parser instance is created per compiled expression and reused
124+
* across all invocations of the returned closure (`evaluate()` resets all
125+
* internal state on every call), avoiding repeated allocations on hot paths.
116126
*/
117127
private compileExpression(expression: string, varNames: string[]): CompiledExpression {
118-
// SECURITY NOTE: Using Function constructor for expression evaluation.
119-
// This is a controlled use case with:
120-
// 1. Sanitization check (isDangerous) performed by caller
121-
// 2. Strict mode enabled ("use strict")
122-
// 3. Limited scope (only varNames variables available)
123-
// 4. No access to global objects (process, window, etc.)
124-
return new Function(...varNames, `"use strict"; return (${expression});`) as CompiledExpression;
128+
// One parser per compiled expression — reused across hot-path calls.
129+
const parser = new SafeExpressionParser();
130+
131+
return (...args: unknown[]) => {
132+
// Reconstruct the named variable context from positional arguments.
133+
const context: Record<string, unknown> = {};
134+
for (let i = 0; i < varNames.length; i++) {
135+
context[varNames[i]] = args[i];
136+
}
137+
return parser.evaluate(expression, context);
138+
};
125139
}
126140

127141
/**

0 commit comments

Comments
 (0)