A production-ready AST security guard for JavaScript - validate, protect, and enforce code safety with extensible rules
npm install @enclave-vm/ast


> A production-ready, extensible AST validator for JavaScript with rule-based validation
AST Guard is a powerful static analysis tool for JavaScript code. It provides a flexible, rule-based architecture for validating Abstract Syntax Trees (AST) with comprehensive built-in rules and support for custom validation logic.
| Metric | Value |
| -------------- | ---------------------------------------------------- |
| CVE Protection | 100% (all known vm2, isolated-vm, node-vm exploits) |
| Security Tests | 613 tests, 100% pass rate |
| Code Coverage | 95%+ |
| Defense Layers | 4 (AST validation, transformation, proxy, isolation) |
Protected Against:
- vm2 CVEs: CVE-2023-29017, CVE-2023-30547, CVE-2023-32313, CVE-2023-37466
- Constructor chain escapes: [].constructor.constructor('code')()
- Prototype pollution: __proto__, Object.setPrototypeOf
- Reflection API bypasses: Reflect.get, Reflect.construct
- Global object access: window, globalThis, this
For detailed CVE analysis, see CVE-COVERAGE.md. For the full list of 100+ blocked attack vectors, see SECURITY-AUDIT.md.
- Production-Ready: Comprehensive error handling, type-safe APIs, and battle-tested rules
- Extensible: Plugin architecture with custom rule support
- Type-Safe: Full TypeScript support with strict typing
- Zero Dependencies: Lightweight with only acorn and acorn-walk as dependencies
- Comprehensive Rules: Built-in rules for common validation scenarios
- Security-Focused: Rules to prevent dangerous code patterns
- Performance: Fast validation with configurable limits
- Detailed Reporting: Rich error messages with source locations
``bash`
npm install ast-guardor
yarn add ast-guardor
pnpm add ast-guard
`typescript
import { JSAstValidator, DisallowedIdentifierRule, NoEvalRule } from 'ast-guard';
// Create validator with built-in rules
const validator = new JSAstValidator([
new DisallowedIdentifierRule({ disallowed: ['eval', 'Function'] }),
new NoEvalRule(),
]);
// Validate code
const result = await validator.validate(
const x = 1;
eval("alert('hello')"););
if (!result.valid) {
console.log('Validation failed:');
result.issues.forEach((issue) => {
console.log(- [${issue.severity}] ${issue.message} (${issue.code})); at line ${issue.location.line}, column ${issue.location.column}
if (issue.location) {
console.log();`
}
});
}
AST Guard provides pre-configured security presets for quick and easy setup. Choose from five security levels, from maximum lockdown to minimal restrictions.
| Preset | Security Level | Description | Use Case |
| -------------- | -------------- | ---------------------------------------------------- | ----------------------------------- |
| STRICT | Maximum | Blocks all dangerous patterns, loops, and async | Untrusted code execution |
| SECURE | High | Blocks most dangerous patterns, allows bounded loops | Semi-trusted code with restrictions |
| STANDARD | Medium | Sensible defaults, blocks critical risks only | Trusted code with safety checks |
| PERMISSIVE | Low | Minimal restrictions, only blocks eval | Internal scripts |
`typescript
import { JSAstValidator, Presets, PresetLevel } from 'ast-guard';
// Option 1: Use the Presets object
const lockdownRules = Presets.strict();
const validator = new JSAstValidator(lockdownRules);
// Option 2: Use createPreset with level
import { createPreset } from 'ast-guard';
const securedRules = createPreset(PresetLevel.SECURE);
const validator = new JSAstValidator(securedRules);
// Option 3: Use individual preset function
import { createStandardPreset } from 'ast-guard';
const balancedRules = createStandardPreset();
const validator = new JSAstValidator(balancedRules);
`
#### STRICT Preset
Maximum security for untrusted code. Bank-grade protection.
Includes all security rules to block known sandbox escape exploits:
- ✅ NoEvalRule - Blocks eval() and Function constructor
- ✅ NoGlobalAccessRule - Blocks constructor chains, Reflect API, global object access
- ✅ NoAsyncRule - Blocks async/await patterns
- ✅ DisallowedIdentifierRule - Blocks dangerous identifiers
- ✅ ForbiddenLoopRule - Blocks loops (or optionally allow bounded loops)
- ✅ And more...
`typescript
import { Presets } from 'ast-guard';
// Maximum lockdown
const rules = Presets.strict();
// With customization
const rules = Presets.strict({
// Allow specific loops
allowedLoops: { allowFor: true, allowForOf: true },
// Require specific API calls
requiredFunctions: ['callTool'],
minFunctionCalls: 1,
maxFunctionCalls: 10,
// Validate function arguments
functionArgumentRules: {
callTool: {
minArgs: 2,
expectedTypes: ['string', 'object'],
},
},
// Add custom blocked identifiers
additionalDisallowedIdentifiers: ['window', 'document'],
});
`
Blocks:
- All eval-like constructs (eval, Function, setTimeout with strings)process
- All loops (for, while, do-while, for-in, for-of)
- All async/await
- Dangerous identifiers: , require, global, __dirname, constructor, __proto__, etc.
#### SECURE Preset
High security with some flexibility for semi-trusted code.
`typescript
import { Presets } from 'ast-guard';
const rules = Presets.secured({
requiredFunctions: ['callTool'],
maxFunctionCalls: 20,
});
`
Blocks:
- All eval-like constructs
- Dangerous identifiers: process, require, global, __dirname
- Infinite loops (while, do-while)
- Async function declarations
Allows:
- Bounded for/for-of loops
- Await expressions (but not async functions)
#### STANDARD Preset
Medium security with sensible defaults for most use cases.
`typescript
import { Presets } from 'ast-guard';
const rules = Presets.balanced({
requiredFunctions: ['callTool'],
additionalDisallowedIdentifiers: ['window', 'document'], // Browser-specific
});
`
Blocks:
- eval() and Function constructor
- Critical identifiers: process, require, eval, Function
- Infinite while/do-while loops
Allows:
- All for loops (for, for-in, for-of)
- Async/await
- Constructor and prototype access
#### PERMISSIVE Preset
Low security for internal or trusted scripts.
`typescript
import { Presets } from 'ast-guard';
const rules = Presets.naive({
requiredFunctions: ['callTool'], // Optional API enforcement
});
`
Blocks:
- eval() only
Allows:
- All loops
- Async/await
- All identifiers
- Constructor and prototype access
All presets accept options to customize their behavior:
`typescript
import { Presets } from 'ast-guard';
const rules = Presets.secured({
// Add more blocked identifiers
additionalDisallowedIdentifiers: ['window', 'document', 'localStorage'],
// Require specific functions
requiredFunctions: ['callTool', 'getTool'],
minFunctionCalls: 1,
maxFunctionCalls: 50,
// Override default loop restrictions
allowedLoops: {
allowFor: true,
allowWhile: true, // Override: allow while in secured preset
allowDoWhile: false,
allowForIn: false,
allowForOf: true,
},
// Override default async restrictions
allowAsync: {
allowAsyncFunctions: true, // Override: allow async in secured preset
allowAwait: true,
},
// Validate specific function arguments
functionArgumentRules: {
callTool: {
minArgs: 2,
maxArgs: 3,
expectedTypes: ['string', 'object'],
validator: (args) => {
// Custom validation
if (args[0]?.value === '') {
return 'Tool name cannot be empty';
}
return null;
},
},
},
});
`
#### Example 1: Untrusted Code Sandbox
`typescript
import { JSAstValidator, Presets } from 'ast-guard';
// Maximum security for untrusted user code
const validator = new JSAstValidator(
Presets.strict({
requiredFunctions: ['callTool'],
functionArgumentRules: {
callTool: {
minArgs: 2,
expectedTypes: ['string', 'object'],
},
},
}),
);
const userCode =
// User-submitted code
callTool("getData", { id: 123 });;
const result = await validator.validate(userCode, {
rules: {
'no-eval': true,
'disallowed-identifier': true,
'forbidden-loop': true,
'required-function-call': true,
'call-argument-validation': true,
},
});
if (result.valid) {
// Safe to execute in sandbox
executeInSandbox(userCode);
}
`
#### Example 2: Plugin System
`typescript
import { JSAstValidator, Presets } from 'ast-guard';
// Balanced security for plugin code
const validator = new JSAstValidator(
Presets.balanced({
requiredFunctions: ['registerPlugin'],
additionalDisallowedIdentifiers: ['process', 'require', 'fs'],
}),
);
const pluginCode = await loadPluginCode();
const result = await validator.validate(pluginCode, {
rules: {
'no-eval': true,
'disallowed-identifier': true,
'required-function-call': true,
},
});
`
#### Example 3: Internal Automation Scripts
`typescript
import { JSAstValidator, Presets } from 'ast-guard';
// Minimal security for trusted internal scripts
const validator = new JSAstValidator(
Presets.naive({
requiredFunctions: ['execute'],
}),
);
`
| Feature | STRICT | SECURE | STANDARD | PERMISSIVE |
| -------------------- | ------ | ------ | -------- | ---------- |
| eval() | ❌ | ❌ | ❌ | ❌ |
| Function() | ❌ | ❌ | ❌ | ✅ |
| for loops | ❌ | ✅ | ✅ | ✅ |
| while loops | ❌ | ❌ | ❌ | ✅ |
| async/await | ❌ | ⚠️ | ✅ | ✅ |
| process | ❌ | ❌ | ❌ | ✅ |
| require | ❌ | ❌ | ❌ | ✅ |
| global | ❌ | ❌ | ✅ | ✅ |
| constructor | ❌ | ✅ | ✅ | ✅ |
| Unreachable code | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
Legend: ❌ Blocked (error) | ⚠️ Detected (warning) | ✅ Allowed
The AgentScript preset uses security-level-aware globals for defense-in-depth:
| Global | STRICT | SECURE | STANDARD | PERMISSIVE |
| ------------------------------------------ | ------ | ------ | -------- | ---------- |
| Core API | | | | |
| callTool | ✅ | ✅ | ✅ | ✅ |Math
| Data Types | | | | |
| , JSON, Array | ✅ | ✅ | ✅ | ✅ |Object
| , String, Number, Date | ✅ | ✅ | ✅ | ✅ |undefined
| Constants | | | | |
| , NaN, Infinity | ✅ | ✅ | ✅ | ✅ |parseInt
| Utility Functions | | | | |
| , parseFloat | ❌ | ✅ | ✅ | ✅ |isNaN
| , isFinite | ❌ | ✅ | ✅ | ✅ |encodeURI
| , decodeURI | ❌ | ✅ | ✅ | ✅ |encodeURIComponent
| , decodeURIComponent | ❌ | ✅ | ✅ | ✅ |console
| Debugging | | | | |
| | ❌ | ❌ | ❌ | ✅ |
Always Blocked: eval, Function, process, require, window, globalThis, constructor, __proto__, Proxy, Reflect, Promise, setTimeout, fetch, WebSocket, Map, Set, Symbol, RegExp, and 50+ other dangerous globals.
The AgentScript preset is specifically designed for safe AI agent orchestration. It provides security rules tailored for executing LLM-generated code that calls tools via callTool().
`typescript
import { JSAstValidator, createAgentScriptPreset } from 'ast-guard';
// Basic usage - secure defaults for AgentScript
const rules = createAgentScriptPreset();
const validator = new JSAstValidator(rules);
// With requireCallTool - enforce that code must call tools
const rules = createAgentScriptPreset({
requireCallTool: true, // Require at least one callTool() invocation
});
`
Options:
| Option | Type | Default | Description |
| --------------------------------- | ---------- | -------------------------- | -------------------------------------------------------------- |
| securityLevel | string | 'STANDARD' | Security level: STRICT, SECURE, STANDARD, PERMISSIVE |requireCallTool
| | boolean | false | Require at least one callTool() invocation in the code |allowedGlobals
| | string[] | (based on securityLevel) | Override allowed globals (takes precedence over securityLevel) |additionalDisallowedIdentifiers
| | string[] | [] | Additional identifiers to block beyond the default set |allowArrowFunctions
| | boolean | true | Whether to allow arrow functions (for array methods) |allowedLoops
| | object | { allowFor, allowForOf } | Override default loop restrictions |reservedPrefixes
| | string[] | ['__ag_', '__safe_'] | Reserved prefixes that user code cannot use |staticCallTarget
| | object | { enabled: true } | Configuration for static call target validation |callToolValidation
| | object | - | Validation rules for callTool arguments |
Example with all options:
`typescript
const rules = createAgentScriptPreset({
// Require the code to actually call tools
requireCallTool: true,
// Customize allowed globals
allowedGlobals: ['callTool', 'getTool', 'Math', 'JSON', 'Array', 'Object'],
// Block additional identifiers
additionalDisallowedIdentifiers: ['fetch', 'XMLHttpRequest'],
// Allow arrow functions for array methods (default: true)
allowArrowFunctions: true,
// Allow specific loops (default: for and for-of allowed)
allowedLoops: {
allowFor: true,
allowForOf: true,
allowWhile: false, // Block while loops
allowDoWhile: false, // Block do-while loops
allowForIn: false, // Block for-in loops
},
// Static call target validation
staticCallTarget: {
enabled: true,
allowedToolNames: ['users:list', 'users:get'], // Optional whitelist
},
});
`
The AgentScript preset is used internally by the enclave-vm package for secure code execution.
Prevents usage of specific identifiers (e.g., eval, Function, process).
`typescript
import { DisallowedIdentifierRule } from 'ast-guard';
const rule = new DisallowedIdentifierRule({
disallowed: ['eval', 'Function', 'process', 'require'],
messageTemplate: 'Access to "{identifier}" is forbidden',
});
`
Prevents usage of loop constructs. Configurable to allow specific loop types.
`typescript
import { ForbiddenLoopRule } from 'ast-guard';
const rule = new ForbiddenLoopRule({
allowFor: false,
allowWhile: false,
allowDoWhile: false,
allowForOf: true, // Allow for-of loops
allowForIn: false,
message: 'Loops are not allowed in this context',
});
`
Ensures specific functions are called at least once.
`typescript
import { RequiredFunctionCallRule } from 'ast-guard';
const rule = new RequiredFunctionCallRule({
required: ['callTool'],
minCalls: 1,
maxCalls: 10,
messageTemplate: 'Must call {function} at least once',
});
`
Mode Options:
- mode: 'all' (default) - All functions in required must be calledmode: 'any'
- - At least one function from required must be called
`typescript`
// Require either callTool or invokeAPI to be called
const rule = new RequiredFunctionCallRule({
required: ['callTool', 'invokeAPI'],
mode: 'any',
minCalls: 1,
});
Detects unreachable code (code after return/throw/break/continue).
`typescript
import { UnreachableCodeRule } from 'ast-guard';
const rule = new UnreachableCodeRule();
// Automatically detects unreachable code
`
Validates function call arguments (count and types).
`typescript
import { CallArgumentValidationRule } from 'ast-guard';
const rule = new CallArgumentValidationRule({
functions: {
callTool: {
minArgs: 2,
maxArgs: 2,
expectedTypes: ['string', 'object'],
},
getTool: {
minArgs: 1,
maxArgs: 1,
expectedTypes: ['string'],
validator: (args, node) => {
// Custom validation logic
const firstArg = args[0];
if (firstArg.type === 'Literal' && firstArg.value === '') {
return 'Tool name cannot be empty';
}
return null; // Valid
},
},
},
});
`
Prevents usage of eval(), Function() constructor, and string-based setTimeout/setInterval.
`typescript
import { NoEvalRule } from 'ast-guard';
const rule = new NoEvalRule();
// Blocks: eval(), new Function(), setTimeout("code", ...)
`
Prevents usage of async/await constructs.
`typescript
import { NoAsyncRule } from 'ast-guard';
const rule = new NoAsyncRule({
allowAsyncFunctions: false,
allowAwait: false,
message: 'Async code is not supported',
});
`
Create your own validation rules by implementing the ValidationRule interface:
`typescript
import { ValidationRule, ValidationContext, ValidationSeverity } from 'ast-guard';
import * as walk from 'acorn-walk';
class NoConsoleRule implements ValidationRule {
readonly name = 'no-console';
readonly description = 'Prevents usage of console methods';
readonly defaultSeverity = ValidationSeverity.WARNING;
readonly enabledByDefault = true;
validate(context: ValidationContext): void {
walk.simple(context.ast as any, {
MemberExpression: (node: any) => {
if (node.object.type === 'Identifier' && node.object.name === 'console') {
context.report({
code: 'NO_CONSOLE',
message: 'Console usage is not recommended',
location: node.loc
? {
line: node.loc.start.line,
column: node.loc.start.column,
}
: undefined,
});
}
},
});
}
}
// Use your custom rule
const validator = new JSAstValidator([new NoConsoleRule()]);
`
`typescript
const result = await validator.validate(code, {
// Parse options (passed to acorn)
parseOptions: {
ecmaVersion: 'latest',
sourceType: 'script',
},
// Rule configuration
rules: {
'disallowed-identifier': true, // Enable with default options
'no-eval': false, // Disable rule
'required-function-call': {
// Enable with custom options
enabled: true,
severity: ValidationSeverity.ERROR,
options: { required: ['callTool'] },
},
},
// Stop on first error
stopOnFirstError: false,
// Maximum number of issues (0 = unlimited)
maxIssues: 100,
});
`
`typescript
const validator = new JSAstValidator();
// Register rules
validator.registerRule(new DisallowedIdentifierRule({ disallowed: ['eval'] }));
validator.registerRule(new NoEvalRule());
// Get all rules
const rules = validator.getRules();
// Get specific rule
const rule = validator.getRule('no-eval');
// Unregister rule
validator.unregisterRule('no-eval');
`
`typescript
interface ValidationResult {
valid: boolean; // true if no errors
issues: ValidationIssue[]; // All issues found
ast?: acorn.Node; // Parsed AST (if parsing succeeded)
parseError?: Error; // Parse error (if parsing failed)
}
interface ValidationIssue {
code: string; // Unique issue code
severity: ValidationSeverity; // ERROR | WARNING | INFO
message: string; // Human-readable message
location?: SourceLocation; // Source location
data?: Record
}
`
All errors extend JSAstValidatorError with specific error types:
`typescript
import {
JSAstValidatorError,
ParseError,
RuleConfigurationError,
ConfigurationError,
RuleNotFoundError,
InvalidSourceError,
} from 'ast-guard';
try {
const result = await validator.validate(code);
} catch (error) {
if (error instanceof ParseError) {
console.error(Parse error at line ${error.line}:, error.message);Rule ${error.ruleName} misconfigured:
} else if (error instanceof InvalidSourceError) {
console.error('Invalid source:', error.message);
} else if (error instanceof RuleConfigurationError) {
console.error(, error.message);`
}
}
`typescript
const result = await validator.validate(code);
const stats = validator.getStats(result, Date.now() - startTime);
console.log(Total issues: ${stats.totalIssues});Errors: ${stats.errors}
console.log();Warnings: ${stats.warnings}
console.log();Duration: ${stats.durationMs}ms
console.log();`
Validate code before executing in a sandboxed environment:
`typescript
const validator = new JSAstValidator([
new DisallowedIdentifierRule({ disallowed: ['eval', 'Function', 'process'] }),
new NoEvalRule(),
new ForbiddenLoopRule(), // Prevent infinite loops
new RequiredFunctionCallRule({ required: ['callTool'] }),
]);
const result = await validator.validate(userCode);
if (result.valid) {
// Safe to execute
vm.runInContext(userCode, sandbox);
}
`
Ensure code follows API contracts:
`typescript`
const validator = new JSAstValidator([
new RequiredFunctionCallRule({ required: ['callTool'] }),
new CallArgumentValidationRule({
functions: {
callTool: {
minArgs: 2,
expectedTypes: ['string', 'object'],
},
},
}),
]);
Detect potentially dangerous code patterns:
`typescript`
const validator = new JSAstValidator([
new NoEvalRule(),
new DisallowedIdentifierRule({
disallowed: ['eval', 'Function', 'require', 'process', 'child_process'],
}),
]);
Detect code quality issues:
`typescript`
const validator = new JSAstValidator([
new UnreachableCodeRule(),
// Add custom rules for your quality standards
]);
`bashRun tests
npm test
Performance
- Parse time: ~1-5ms for typical scripts (depends on size)
- Validation time: ~1-10ms per rule (depends on complexity)
- Memory: Minimal overhead, AST is shared across rules
Comparison
| Feature | AST Guard | ESLint | TypeScript |
| ---------------------- | -------------- | ------------ | ------------- |
| Focus | AST validation | Code linting | Type checking |
| Extensible | ✅ | ✅ | ⚠️ |
| Lightweight | ✅ | ❌ | ❌ |
| Type-safe | ✅ | ⚠️ | ✅ |
| Security rules | ✅ | ⚠️ | ❌ |
| Runtime validation | ✅ | ❌ | ❌ |
Defense-in-Depth Architecture
AST Guard provides 4 layers of protection:
`text
Layer 1: AST Validation
├── NoEvalRule - Blocks eval(), Function()
├── NoGlobalAccessRule - Blocks window, globalThis, .constructor
├── NoAsyncRule - Blocks async/await (optional)
├── DisallowedIdentifierRule - Blocks dangerous identifiers
└── ForbiddenLoopRule - Blocks/transforms loopsLayer 2: AST Transformation
├── Direct identifiers: console → __safe_console
├── Computed access: obj['eval'] → obj['__safe_eval']
└── Static string literals transformed
Layer 3: Runtime Proxy Layer
├── __safe_callTool() - Validates tool calls
├── __safe_console() - Captures logs
└── All proxied functions enforce security
Layer 4: Worker Isolation
├── Separate worker thread
├── Sandboxed VM context
└── No access to Node.js globals
`$3
These are inherent to any static analyzer (not vulnerabilities):
| Limitation | Example | Mitigation |
| ---------------------------- | -------------------- | --------------------------------- |
| Computed property access |
obj['constructor'] | Object.freeze(Object.prototype) |
| Runtime string construction | 'con' + 'structor' | VM isolation |
| Destructuring property names | { constructor: c }` | Freeze prototypes || Document | Purpose |
| ------------------------------------------------------------ | ----------------------------------------------- |
| docs/AGENTSCRIPT.md | AgentScript language reference for AI agents |
| docs/CVE-COVERAGE.md | Detailed CVE analysis and protection mechanisms |
| docs/SECURITY-AUDIT.md | Full list of 67+ blocked attack vectors |
| docs/STDLIB-SECURITY.md | Standard library security analysis |
| docs/LOOP-TRANSFORMATION.md | Future loop transformation design |
- [x] Core validator architecture
- [x] Built-in security rules
- [x] Argument validation
- [x] Unreachable code detection
- [x] Comprehensive test suite (613 tests)
- [x] CVE protection (vm2, isolated-vm, node-vm)
- [ ] Loop transformation (design complete)
- [ ] CLI tool
- [ ] VS Code extension
Contributions are welcome! Please see CONTRIBUTING.md for details.
Apache-2.0
Built with:
- acorn - JavaScript parser
- acorn-walk - AST walker
- Part of the FrontMCP ecosystem