its-compiler-js
Version:
JavaScript/TypeScript implementation of the Instruction Template Specification (ITS) compiler
241 lines • 10 kB
JavaScript
/**
* Conditional expression evaluation for ITS Compiler using jsep for safety
*/
import jsep from 'jsep';
import { ITSSecurityError } from './types.js';
export class ConditionalEvaluator {
constructor(maxExpressionLength = 500) {
this.maxExpressionLength = maxExpressionLength;
}
/**
* Evaluate conditionals in content and return filtered content
*/
evaluateContent(content, variables) {
const result = [];
for (const element of content) {
if (element.type === 'conditional') {
const conditionalElement = element;
const conditionResult = this.evaluateCondition(conditionalElement.condition, variables);
if (conditionResult) {
// Include content from the 'content' array
const nestedContent = this.evaluateContent(conditionalElement.content, variables);
result.push(...nestedContent);
}
else if (conditionalElement.else) {
// Include content from the 'else' array
const elseContent = this.evaluateContent(conditionalElement.else, variables);
result.push(...elseContent);
}
}
else {
// Non-conditional element, include as-is
result.push(element);
}
}
return result;
}
/**
* Evaluate a conditional expression using jsep for safety
*/
evaluateCondition(condition, variables) {
// Basic security validation
this.validateConditionSecurity(condition);
try {
// Parse the expression into AST
const ast = jsep(condition);
// Evaluate the AST safely
const result = this.evaluateASTNode(ast, variables);
return Boolean(result);
}
catch (error) {
throw new ITSSecurityError(`Error evaluating condition '${condition}': ${error}`, 'conditional_evaluation', 'EXPRESSION_ERROR');
}
}
/**
* Security validation for conditional expressions
*/
validateConditionSecurity(condition) {
if (condition.length > this.maxExpressionLength) {
throw new ITSSecurityError(`Condition too long: ${condition.length} characters`, 'expression_length', 'SIZE_LIMIT_EXCEEDED');
}
// Check for dangerous patterns that might bypass jsep
const dangerousPatterns = [
/\b(eval|exec|function|Function|setTimeout|setInterval)\s*\(/i,
/\b(import|require|global|window|document|process)\b/i,
/__\w+__/,
/\.\s*constructor\s*\./,
/\.\s*prototype\s*\./,
/\.\s*__proto__\s*\./,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(condition)) {
throw new ITSSecurityError('Dangerous pattern detected in condition', 'expression_validation', 'MALICIOUS_CONTENT');
}
}
}
/**
* Safely evaluate an AST node
*/
evaluateASTNode(node, variables) {
if (!node || typeof node !== 'object') {
throw new Error('Invalid AST node');
}
switch (node.type) {
case 'Literal':
return node.value;
case 'Identifier':
if (!(node.name in variables)) {
throw new Error(`Variable '${node.name}' is not defined`);
}
return variables[node.name];
case 'MemberExpression': {
const object = this.evaluateASTNode(node.object, variables);
if (object === null || object === undefined) {
throw new Error('Cannot access property of null or undefined');
}
let property;
if (node.computed) {
// For array access like obj[0] or obj[key]
property = this.evaluateASTNode(node.property, variables);
}
else {
// For property access like obj.prop
property = node.property.name;
}
// Handle special properties
if (property === 'length' && (Array.isArray(object) || typeof object === 'string')) {
return object.length;
}
// Validate property access for security
this.validatePropertyAccess(object, property);
return object[property];
}
case 'BinaryExpression': {
const left = this.evaluateASTNode(node.left, variables);
const right = this.evaluateASTNode(node.right, variables);
switch (node.operator) {
case '==':
return left == right;
case '===':
return left === right;
case '!=':
return left != right;
case '!==':
return left !== right;
case '<':
return left < right;
case '<=':
return left <= right;
case '>':
return left > right;
case '>=':
return left >= right;
// Handle logical operators in case jsep treats them as binary
case '&&':
return left && right;
case '||':
return left || right;
default:
throw new Error(`Unsupported binary operator: ${node.operator}`);
}
}
case 'LogicalExpression': {
const leftVal = this.evaluateASTNode(node.left, variables);
switch (node.operator) {
case '&&':
// Short-circuit evaluation
return leftVal && this.evaluateASTNode(node.right, variables);
case '||':
// Short-circuit evaluation
return leftVal || this.evaluateASTNode(node.right, variables);
case 'and':
// Support 'and' keyword as well
return leftVal && this.evaluateASTNode(node.right, variables);
case 'or':
// Support 'or' keyword as well
return leftVal || this.evaluateASTNode(node.right, variables);
default:
throw new Error(`Unsupported logical operator: ${node.operator}`);
}
}
case 'UnaryExpression': {
switch (node.operator) {
case '!':
return !this.evaluateASTNode(node.argument, variables);
case '-':
return -this.evaluateASTNode(node.argument, variables);
case '+':
return +this.evaluateASTNode(node.argument, variables);
default:
throw new Error(`Unsupported unary operator: ${node.operator}`);
}
}
case 'ArrayExpression':
// Allow simple array literals like [1, 2, 3]
return node.elements.map((element) => this.evaluateASTNode(element, variables));
default:
throw new Error(`Unsupported expression type: ${node.type}`);
}
}
/**
* Validate property access for security
*/
validatePropertyAccess(object, property) {
// Block access to dangerous properties
const dangerousProperties = new Set([
'constructor',
'prototype',
'__proto__',
'__defineGetter__',
'__defineSetter__',
'__lookupGetter__',
'__lookupSetter__',
]);
if (typeof property === 'string' && dangerousProperties.has(property)) {
throw new ITSSecurityError(`Access to dangerous property '${property}' is blocked`, 'property_access', 'MALICIOUS_CONTENT');
}
// Additional validation for function objects
if (typeof object === 'function') {
throw new ITSSecurityError('Property access on function objects is not allowed', 'function_property_access', 'MALICIOUS_CONTENT');
}
// Validate array bounds
if (Array.isArray(object) && typeof property === 'number') {
if (property < 0) {
// Support negative indexing
const actualIndex = object.length + property;
if (actualIndex < 0 || actualIndex >= object.length) {
throw new Error(`Array index ${property} out of bounds`);
}
}
else if (property >= object.length) {
throw new Error(`Array index ${property} out of bounds`);
}
}
}
/**
* Validate condition syntax using jsep
*/
validateCondition(condition, variables) {
const errors = [];
try {
this.validateConditionSecurity(condition);
// Try to parse with jsep
const ast = jsep(condition);
// Try to evaluate
this.evaluateASTNode(ast, variables);
}
catch (error) {
if (error instanceof ITSSecurityError) {
errors.push(error.message);
}
else if (error instanceof Error) {
errors.push(`Condition validation failed: ${error.message}`);
}
else {
errors.push(`Condition validation failed: ${error}`);
}
}
return errors;
}
}
//# sourceMappingURL=conditional-evaluator.js.map