import { parse } from '@babel/parser'; import { ALLOWED_GLOBALS, ALLOWED_INSTANCE_METHODS, ALLOWED_METHODS, } from './constants'; import { collectDeclaredIdentifiers, isPropertyKey, walkNode, } from './ast-walker'; /** * Validates that a JavaScript function is safe to execute * by checking the AST for allowed operations only (allowlist approach) */ export function validate(code: string): { valid: boolean; error?: string; } { if (!code || typeof code !== 'string') { return { valid: false, error: 'Code must be a non-empty string' }; } try { // Parse the code to AST const ast = parse(code, { sourceType: 'module', allowReturnOutsideFunction: true, plugins: ['typescript'], }); // Validate root structure: must be exactly one function expression const program = ast.program; const body = program.body; if (body.length === 0) { return { valid: false, error: 'Code cannot be empty' }; } if (body.length > 1) { return { valid: false, error: 'Code must contain only a single function. Multiple statements are not allowed.', }; } const rootStatement = body[0]!; // Must be an expression statement containing a function if (rootStatement.type !== 'ExpressionStatement') { if (rootStatement.type === 'VariableDeclaration') { return { valid: false, error: 'Variable declarations (const, let, var) are not allowed. Use a direct function expression instead.', }; } if (rootStatement.type === 'FunctionDeclaration') { return { valid: false, error: 'Function declarations are not allowed. Use an arrow function or function expression instead: (payload) => { ... } or function(payload) { ... }', }; } return { valid: false, error: 'Code must be a function expression or arrow function', }; } const rootExpression = rootStatement.expression; if (rootExpression.type !== 'ArrowFunctionExpression') { if (rootExpression.type === 'FunctionExpression') { return { valid: false, error: 'Function expressions are not allowed. Use arrow functions instead: (payload) => { ... }', }; } return { valid: false, error: 'Code must be an arrow function, e.g.: (payload) => { ... }', }; } // Collect all declared identifiers (variables, parameters) const declaredIdentifiers = collectDeclaredIdentifiers(ast); let validationError: string | undefined; // Walk the AST to check for allowed patterns only walkNode(ast, (node, parent) => { // Skip if we already found an error if (validationError) { return; } // Block import/export declarations if ( node.type === 'ImportDeclaration' || node.type === 'ExportDeclaration' ) { validationError = 'import/export statements are not allowed'; return; } // Block function declarations inside the function body // (FunctionDeclaration creates a named function, not allowed) if (node.type === 'FunctionDeclaration') { validationError = 'Named function declarations are not allowed inside the function body.'; return; } // Block loops - use array methods like .map(), .filter() instead if ( node.type === 'WhileStatement' || node.type === 'DoWhileStatement' || node.type === 'ForStatement' || node.type === 'ForInStatement' || node.type === 'ForOfStatement' ) { validationError = 'Loops are not allowed. Use array methods like .map(), .filter(), .reduce() instead.'; return; } // Block advanced/dangerous features if (node.type === 'TryStatement') { validationError = 'try/catch statements are not allowed'; return; } if (node.type === 'ThrowStatement') { validationError = 'throw statements are not allowed'; return; } if (node.type === 'WithStatement') { validationError = 'with statements are not allowed'; return; } if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') { validationError = 'Class definitions are not allowed'; return; } if (node.type === 'AwaitExpression') { validationError = 'async/await is not allowed'; return; } if (node.type === 'YieldExpression') { validationError = 'Generators are not allowed'; return; } // Block 'this' keyword - arrow functions don't have their own 'this' // but we block it entirely to prevent any scope leakage if (node.type === 'ThisExpression') { validationError = "'this' keyword is not allowed. Use the payload parameter instead."; return; } // Check identifiers that reference globals if (node.type === 'Identifier') { const name = node.name as string; // Block 'arguments' - not available in arrow functions anyway // but explicitly block to prevent any confusion if (name === 'arguments') { validationError = "'arguments' is not allowed. Use explicit parameters instead."; return; } // Skip if it's a property key (not a value reference) if (isPropertyKey(node, parent)) { return; } // Skip if it's a declared local variable/parameter if (declaredIdentifiers.has(name)) { return; } // Check if it's an allowed global if (!ALLOWED_GLOBALS.has(name)) { validationError = `Use of '${name}' is not allowed. Only safe built-in functions are permitted.`; return; } } // Check method calls on global objects (like Math.random, JSON.parse) // Handles both regular calls and optional chaining (?.) if ( node.type === 'CallExpression' || node.type === 'OptionalCallExpression' ) { const callee = node.callee as Record; const isMemberExpr = callee.type === 'MemberExpression' || callee.type === 'OptionalMemberExpression'; if (isMemberExpr) { const obj = callee.object as Record; const prop = callee.property as Record; const computed = callee.computed as boolean; // Static method call on global object: Math.random(), JSON.parse() if ( obj.type === 'Identifier' && prop.type === 'Identifier' && !computed ) { const objName = obj.name as string; const methodName = prop.name as string; // Check if it's a call on an allowed global object if (ALLOWED_GLOBALS.has(objName) && ALLOWED_METHODS[objName]) { if (!ALLOWED_METHODS[objName].has(methodName)) { validationError = `Method '${objName}.${methodName}' is not allowed. Only safe methods are permitted.`; return; } } } // Instance method call: arr.map(), str.toLowerCase(), arr?.map() // We allow these if the method name is in ALLOWED_INSTANCE_METHODS if (prop.type === 'Identifier' && !computed) { const methodName = prop.name as string; // If calling on something other than an allowed global, // check if the method is in the allowed instance methods if ( obj.type !== 'Identifier' || !ALLOWED_GLOBALS.has(obj.name as string) ) { if (!ALLOWED_INSTANCE_METHODS.has(methodName)) { validationError = `Method '.${methodName}()' is not allowed. Only safe methods are permitted.`; return; } } } } } // Check 'new' expressions - only allow new Date() if (node.type === 'NewExpression') { const callee = node.callee as Record; if (callee.type === 'Identifier') { const name = callee.name as string; if (name !== 'Date') { validationError = `'new ${name}()' is not allowed. Only 'new Date()' is permitted.`; return; } } } }); if (validationError) { return { valid: false, error: validationError }; } return { valid: true }; } catch (error) { return { valid: false, error: error instanceof Error ? `Parse error: ${error.message}` : 'Unknown parse error', }; } }