AST Guard

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.
Bank-Grade Security
Hardened Against All Known Sandbox Escape Exploits
| 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.
Features
- 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
Installation
npm install ast-guard
yarn add ast-guard
pnpm add ast-guard
Quick Start
import { JSAstValidator, DisallowedIdentifierRule, NoEvalRule } from 'ast-guard';
const validator = new JSAstValidator([
new DisallowedIdentifierRule({ disallowed: ['eval', 'Function'] }),
new NoEvalRule(),
]);
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})`);
if (issue.location) {
console.log(` at line ${issue.location.line}, column ${issue.location.column}`);
}
});
}
Presets
AST Guard provides pre-configured security presets for quick and easy setup. Choose from five security levels, from maximum lockdown to minimal restrictions.
Available Presets
| 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 |
Quick Start with Presets
import { JSAstValidator, Presets, PresetLevel } from 'ast-guard';
const lockdownRules = Presets.strict();
const validator = new JSAstValidator(lockdownRules);
import { createPreset } from 'ast-guard';
const securedRules = createPreset(PresetLevel.SECURE);
const validator = new JSAstValidator(securedRules);
import { createStandardPreset } from 'ast-guard';
const balancedRules = createStandardPreset();
const validator = new JSAstValidator(balancedRules);
Preset Details
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...
import { Presets } from 'ast-guard';
const rules = Presets.strict();
const rules = Presets.strict({
allowedLoops: { allowFor: true, allowForOf: true },
requiredFunctions: ['callTool'],
minFunctionCalls: 1,
maxFunctionCalls: 10,
functionArgumentRules: {
callTool: {
minArgs: 2,
expectedTypes: ['string', 'object'],
},
},
additionalDisallowedIdentifiers: ['window', 'document'],
});
Blocks:
- All eval-like constructs (
eval, Function, setTimeout with strings)
- All loops (for, while, do-while, for-in, for-of)
- All async/await
- Dangerous identifiers:
process, require, global, __dirname, constructor, __proto__, etc.
SECURE Preset
High security with some flexibility for semi-trusted code.
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.
import { Presets } from 'ast-guard';
const rules = Presets.balanced({
requiredFunctions: ['callTool'],
additionalDisallowedIdentifiers: ['window', 'document'],
});
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.
import { Presets } from 'ast-guard';
const rules = Presets.naive({
requiredFunctions: ['callTool'],
});
Blocks:
Allows:
- All loops
- Async/await
- All identifiers
- Constructor and prototype access
Customizing Presets
All presets accept options to customize their behavior:
import { Presets } from 'ast-guard';
const rules = Presets.secured({
additionalDisallowedIdentifiers: ['window', 'document', 'localStorage'],
requiredFunctions: ['callTool', 'getTool'],
minFunctionCalls: 1,
maxFunctionCalls: 50,
allowedLoops: {
allowFor: true,
allowWhile: true,
allowDoWhile: false,
allowForIn: false,
allowForOf: true,
},
allowAsync: {
allowAsyncFunctions: true,
allowAwait: true,
},
functionArgumentRules: {
callTool: {
minArgs: 2,
maxArgs: 3,
expectedTypes: ['string', 'object'],
validator: (args) => {
if (args[0]?.value === '') {
return 'Tool name cannot be empty';
}
return null;
},
},
},
});
Real-World Examples
Example 1: Untrusted Code Sandbox
import { JSAstValidator, Presets } from 'ast-guard';
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) {
executeInSandbox(userCode);
}
Example 2: Plugin System
import { JSAstValidator, Presets } from 'ast-guard';
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
import { JSAstValidator, Presets } from 'ast-guard';
const validator = new JSAstValidator(
Presets.naive({
requiredFunctions: ['execute'],
}),
);
Preset Comparison Matrix
| eval() | ❌ | ❌ | ❌ | ❌ |
| Function() | ❌ | ❌ | ❌ | ✅ |
| for loops | ❌ | ✅ | ✅ | ✅ |
| while loops | ❌ | ❌ | ❌ | ✅ |
| async/await | ❌ | ⚠️ | ✅ | ✅ |
| process | ❌ | ❌ | ❌ | ✅ |
| require | ❌ | ❌ | ❌ | ✅ |
| global | ❌ | ❌ | ✅ | ✅ |
| constructor | ❌ | ✅ | ✅ | ✅ |
| Unreachable code | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
Legend: ❌ Blocked (error) | ⚠️ Detected (warning) | ✅ Allowed
AgentScript Globals by Security Level
The AgentScript preset uses security-level-aware globals for defense-in-depth:
| Core API | | | | |
callTool | ✅ | ✅ | ✅ | ✅ |
| Data Types | | | | |
Math, JSON, Array | ✅ | ✅ | ✅ | ✅ |
Object, String, Number, Date | ✅ | ✅ | ✅ | ✅ |
| Constants | | | | |
undefined, NaN, Infinity | ✅ | ✅ | ✅ | ✅ |
| Utility Functions | | | | |
parseInt, parseFloat | ❌ | ✅ | ✅ | ✅ |
isNaN, isFinite | ❌ | ✅ | ✅ | ✅ |
encodeURI, decodeURI | ❌ | ✅ | ✅ | ✅ |
encodeURIComponent, decodeURIComponent | ❌ | ✅ | ✅ | ✅ |
| Debugging | | | | |
console | ❌ | ❌ | ❌ | ✅ |
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.
AgentScript Preset
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().
import { JSAstValidator, createAgentScriptPreset } from 'ast-guard';
const rules = createAgentScriptPreset();
const validator = new JSAstValidator(rules);
const rules = createAgentScriptPreset({
requireCallTool: true,
});
Options:
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:
const rules = createAgentScriptPreset({
requireCallTool: true,
allowedGlobals: ['callTool', 'getTool', 'Math', 'JSON', 'Array', 'Object'],
additionalDisallowedIdentifiers: ['fetch', 'XMLHttpRequest'],
allowArrowFunctions: true,
allowedLoops: {
allowFor: true,
allowForOf: true,
allowWhile: false,
allowDoWhile: false,
allowForIn: false,
},
staticCallTarget: {
enabled: true,
allowedToolNames: ['users:list', 'users:get'],
},
});
The AgentScript preset is used internally by the enclave-vm package for secure code execution.
Built-in Rules
DisallowedIdentifierRule
Prevents usage of specific identifiers (e.g., eval, Function, process).
import { DisallowedIdentifierRule } from 'ast-guard';
const rule = new DisallowedIdentifierRule({
disallowed: ['eval', 'Function', 'process', 'require'],
messageTemplate: 'Access to "{identifier}" is forbidden',
});
ForbiddenLoopRule
Prevents usage of loop constructs. Configurable to allow specific loop types.
import { ForbiddenLoopRule } from 'ast-guard';
const rule = new ForbiddenLoopRule({
allowFor: false,
allowWhile: false,
allowDoWhile: false,
allowForOf: true,
allowForIn: false,
message: 'Loops are not allowed in this context',
});
RequiredFunctionCallRule
Ensures specific functions are called at least once.
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 called
mode: 'any' - At least one function from required must be called
const rule = new RequiredFunctionCallRule({
required: ['callTool', 'invokeAPI'],
mode: 'any',
minCalls: 1,
});
UnreachableCodeRule
Detects unreachable code (code after return/throw/break/continue).
import { UnreachableCodeRule } from 'ast-guard';
const rule = new UnreachableCodeRule();
CallArgumentValidationRule
Validates function call arguments (count and types).
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) => {
const firstArg = args[0];
if (firstArg.type === 'Literal' && firstArg.value === '') {
return 'Tool name cannot be empty';
}
return null;
},
},
},
});
NoEvalRule
Prevents usage of eval(), Function() constructor, and string-based setTimeout/setInterval.
import { NoEvalRule } from 'ast-guard';
const rule = new NoEvalRule();
NoAsyncRule
Prevents usage of async/await constructs.
import { NoAsyncRule } from 'ast-guard';
const rule = new NoAsyncRule({
allowAsyncFunctions: false,
allowAwait: false,
message: 'Async code is not supported',
});
Custom Rules
Create your own validation rules by implementing the ValidationRule interface:
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,
});
}
},
});
}
}
const validator = new JSAstValidator([new NoConsoleRule()]);
Configuration
Validator Configuration
const result = await validator.validate(code, {
parseOptions: {
ecmaVersion: 'latest',
sourceType: 'script',
},
rules: {
'disallowed-identifier': true,
'no-eval': false,
'required-function-call': {
enabled: true,
severity: ValidationSeverity.ERROR,
options: { required: ['callTool'] },
},
},
stopOnFirstError: false,
maxIssues: 100,
});
Rule Management
const validator = new JSAstValidator();
validator.registerRule(new DisallowedIdentifierRule({ disallowed: ['eval'] }));
validator.registerRule(new NoEvalRule());
const rules = validator.getRules();
const rule = validator.getRule('no-eval');
validator.unregisterRule('no-eval');
Validation Results
interface ValidationResult {
valid: boolean;
issues: ValidationIssue[];
ast?: acorn.Node;
parseError?: Error;
}
interface ValidationIssue {
code: string;
severity: ValidationSeverity;
message: string;
location?: SourceLocation;
data?: Record<string, unknown>;
}
Error Handling
All errors extend JSAstValidatorError with specific error types:
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);
} else if (error instanceof InvalidSourceError) {
console.error('Invalid source:', error.message);
} else if (error instanceof RuleConfigurationError) {
console.error(`Rule ${error.ruleName} misconfigured:`, error.message);
}
}
Statistics
const result = await validator.validate(code);
const stats = validator.getStats(result, Date.now() - startTime);
console.log(`Total issues: ${stats.totalIssues}`);
console.log(`Errors: ${stats.errors}`);
console.log(`Warnings: ${stats.warnings}`);
console.log(`Duration: ${stats.durationMs}ms`);
Use Cases
1. Sandboxed Code Execution
Validate code before executing in a sandboxed environment:
const validator = new JSAstValidator([
new DisallowedIdentifierRule({ disallowed: ['eval', 'Function', 'process'] }),
new NoEvalRule(),
new ForbiddenLoopRule(),
new RequiredFunctionCallRule({ required: ['callTool'] }),
]);
const result = await validator.validate(userCode);
if (result.valid) {
vm.runInContext(userCode, sandbox);
}
2. API Contract Validation
Ensure code follows API contracts:
const validator = new JSAstValidator([
new RequiredFunctionCallRule({ required: ['callTool'] }),
new CallArgumentValidationRule({
functions: {
callTool: {
minArgs: 2,
expectedTypes: ['string', 'object'],
},
},
}),
]);
3. Security Analysis
Detect potentially dangerous code patterns:
const validator = new JSAstValidator([
new NoEvalRule(),
new DisallowedIdentifierRule({
disallowed: ['eval', 'Function', 'require', 'process', 'child_process'],
}),
]);
4. Code Quality Checks
Detect code quality issues:
const validator = new JSAstValidator([
new UnreachableCodeRule(),
]);
Testing
npm test
npm run test:coverage
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
| 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:
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 loops
Layer 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
Known Limitations
These are inherent to any static analyzer (not vulnerabilities):
| Computed property access | obj['constructor'] | Object.freeze(Object.prototype) |
| Runtime string construction | 'con' + 'structor' | VM isolation |
| Destructuring property names | { constructor: c } | Freeze prototypes |
Documentation
Roadmap
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
License
Apache-2.0
Credits
Built with: