Expression Parser Security
Dynamic forms use expressions for conditional logic and dynamic values. When you use JavaScript expressions with type: 'javascript', the expression parser evaluates them safely, preventing code injection attacks while maintaining the flexibility you need.
How JavaScript Expressions Work
When you configure JavaScript expressions in your form:
{
key: 'driverLicense',
type: 'input',
value: '',
label: 'Driver License',
logic: [
{
type: 'required',
condition: {
type: 'javascript',
expression: 'formValue.age >= 18 && formValue.needsTransport === true'
},
}
]
}The expression 'formValue.age >= 18 && formValue.needsTransport === true' is evaluated by the parser, which has access to:
{
fieldValue: currentFieldValue,
formValue: { age: 25, needsTransport: true, ... },
fieldPath: 'driverLicense'
}Security Model
The parser uses different rules for methods and properties:
Methods: Whitelist Only
Only safe methods can be called in expressions:
// ✅ Works - safe string methods
{
type: 'hidden',
condition: {
type: 'javascript',
expression: 'formValue.email.includes("@company.com")'
}
}
// ✅ Works - safe array methods
{
type: 'hidden',
condition: {
type: 'javascript',
expression: 'formValue.roles.some((role) => role === "admin")'
}
}
// ❌ Blocked - not whitelisted
{
type: 'hidden',
condition: {
type: 'javascript',
expression: 'formValue.text.link("url")' // Error: Method "link" is not allowed
}
}Whitelisted Methods Reference
| Category | Methods |
|---|---|
| String | toUpperCase, toLowerCase, trim, includes, startsWith, endsWith, slice, substring, charAt, indexOf, lastIndexOf, split, replace, match |
| Array | map, filter, some, every, find, findIndex, includes, indexOf, join, slice, flat, flatMap |
| Number | toFixed, toString, valueOf |
| Object | hasOwnProperty, toString, valueOf |
Properties: Open Access (Except Dangerous Ones)
All properties in the form data are accessible, except prototype properties:
// ✅ Works - access any form field
expression: 'formValue.firstName';
expression: 'formValue._internalFlag';
expression: 'formValue.nested.deeply.property';
// ❌ Blocked - prototype pollution risks
expression: 'formValue.constructor'; // Error: Property "constructor" is not accessible
expression: 'formValue.__proto__'; // Error: Property "__proto__" is not accessibleWhy? Dynamic forms need to access any field name you define. The parser blocks only the dangerous properties that could break security.
Safe Member Access (Built-in)
Important: Member access is safe by default - accessing properties on null or undefined returns undefined instead of throwing errors:
// ✅ All of these work safely, even when intermediate values are null/undefined
expression: 'formValue.user.profile.firstName';
expression: 'formValue.address.city.toLowerCase()';
expression: 'formValue.settings.notifications.email';
// If formValue.user is null, the expression returns undefined (no error thrown)
// If formValue.user.profile is null, the expression returns undefined (no error thrown)
// No manual null checks needed!This means you don't need to write defensive null checks:
// ❌ Unnecessary - Don't do this
expression: '!formValue.user || !formValue.user.profile || !formValue.user.profile.firstName || fieldValue !== formValue.user.profile.firstName';
// ✅ Better - Safe by default
expression: '!formValue.user.profile.firstName || fieldValue !== formValue.user.profile.firstName';The parser automatically handles null/undefined at every level of property access, making expressions cleaner and more maintainable.
Available Context Variables
When expressions are evaluated, they have access to:
{
fieldValue: 'current field value',
formValue: { /* entire form state */ },
fieldPath: 'fieldName'
}Note: Don't store sensitive data (tokens, API keys) in form state since it's accessible in expressions.
Common Form Use Cases
Dynamic Visibility
{
key: 'companyName',
type: 'input',
value: '',
label: 'Company Name',
// Show only if user selects "Employed"
logic: [
{
type: 'hidden',
condition: {
type: 'javascript',
expression: 'formValue.employmentStatus !== "employed"'
}
}
]
}Conditional Required
{
key: 'taxId',
type: 'input',
value: '',
label: 'Tax ID',
// Required only for business accounts
logic: [
{
type: 'required',
condition: {
type: 'javascript',
expression: 'formValue.accountType === "business"'
},
}
]
}Complex Conditions
{
key: 'personalConcierge',
type: 'checkbox',
value: false,
label: 'Request Personal Concierge Service',
logic: [
{
type: 'hidden',
condition: {
type: 'javascript',
expression: 'formValue.membershipLevel !== "vip" || formValue.annualIncome < 100000'
}
}
]
}String Methods in Expressions
{
key: 'discountCode',
type: 'input',
value: '',
label: 'Discount Code',
logic: [
{
type: 'required',
condition: {
type: 'javascript',
expression: 'formValue.email.endsWith("@company.com")'
},
}
]
}What the Parser Prevents
For dynamic forms, the parser prevents:
- ✅ Code Injection: Can't execute
Function(),eval(), or create new code - ✅ Prototype Pollution: Can't access
constructoror__proto__ - ✅ Unsafe Operations: Can't call methods that modify state or access globals
Custom Functions
When providing custom functions for use in expressions, register them with the FunctionRegistryService:
// ✅ GOOD - Pure functions
import { FunctionRegistryService } from '@ng-forge/dynamic-forms';
const functionRegistry = inject(FunctionRegistryService );
functionRegistry.registerCustomFunction('isValidEmail', (ctx) => {
return /^[^s@]+@[^s@]+.[^s@]+$/.test(ctx.fieldValue);
});
functionRegistry.registerCustomFunction('isAdult', (ctx) => {
return ctx.formValue.age >= 18;
});Then use them with type: 'custom':
{
key: 'adultContent',
type: 'checkbox',
value: false,
label: 'Access Adult Content',
logic: [
{
type: 'hidden',
condition: {
type: 'custom',
expression: 'isAdult'
}
}
]
}Important: Only provide pure functions without side effects:
// ❌ BAD - Side effects
functionRegistry.registerCustomFunction('logValue', (ctx) => {
console.log(ctx.fieldValue); // Side effect!
trackAnalytics(ctx); // Side effect!
return true;
});Supported Expression Features
In JavaScript expressions (type: 'javascript'), you can use:
| Feature | Examples |
|---|---|
| Property access | formValue.name, formValue.user.profile.email |
| Comparisons | ===, !==, >, <, >=, <= |
| Logical operators | &&, ||, ! |
| Arithmetic | +, -, *, /, % |
| String methods | See |
| Array methods | See |
Example:
expression: 'formValue.age >= 18 && formValue.email.includes("@example.com")';Not Supported: Object literals {}, ternary a ? b : c, assignment x = 5, new keyword, function declarations
Summary
The expression parser lets you write flexible conditional logic while preventing code injection attacks:
- Form state is accessible - Any field in
formValuecan be read - Methods are restricted - Only safe, non-mutating methods allowed
- Prototype is protected - Can't access dangerous properties
- Custom functions supported - Register pure functions for reusable logic