
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
@n8n/expression-runtime
Advanced tools
Secure, isolated expression evaluation runtime for n8n workflows.
Shipped — the vm engine is n8n's default expression engine.
IsolatedVmBridge: V8 isolate management via isolated-vmExpressionEvaluator: tournament integration, expression code caching, isolate poolingN8N_EXPRESSION_ENGINE=legacy opts outpackages/cliComing later:
This package provides a secure runtime for evaluating expressions in isolated contexts.
Currently supports:
isolated-vm for V8 isolate-based isolation with lazy data loadingFuture support (Phase 2+):
ObservabilityProviderThisSanitizer, PrototypeSanitizer, DollarSignValidator) validate expressions before executionThe runtime uses a three-layer architecture:
See ARCHITECTURE.md for detailed design documentation.
pnpm add @n8n/expression-runtime
import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
// Create evaluator with a bridge factory (bridges are pooled)
const evaluator = new ExpressionEvaluator({
createBridge: () => new IsolatedVmBridge({ memoryLimit: 128, timeout: 5000 }),
maxCodeCacheSize: 1024,
});
// Initialize
await evaluator.initialize();
// Acquire an isolate for a caller, evaluate, release
const caller = {};
await evaluator.acquire(caller);
const result = evaluator.evaluate(
'{{ $json.user.email }}',
{
$json: {
user: { email: 'test@example.com' }
}
},
caller,
);
console.log(result); // "test@example.com"
await evaluator.release(caller);
// Clean up
await evaluator.dispose();
Pass AST security hooks from expression-sandboxing.ts to enable full security validation. This is the pattern used by the workflow package:
import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
import {
ThisSanitizer,
PrototypeSanitizer,
DollarSignValidator,
} from 'n8n-workflow/expression-sandboxing';
const evaluator = new ExpressionEvaluator({
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
maxCodeCacheSize: 1024,
hooks: {
before: [ThisSanitizer],
after: [PrototypeSanitizer, DollarSignValidator],
},
});
await evaluator.initialize();
When hooks is omitted the evaluator still runs tournament transformation (template parsing, this binding) but without AST security validation — suitable for development and testing.
Pass an ObservabilityProvider implementation to emit metrics, traces, and logs for evaluations:
const evaluator = new ExpressionEvaluator({
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
maxCodeCacheSize: 1024,
observability,
});
This package defines the ObservabilityProvider interface; the production implementation lives in packages/cli/src/expression-observability/expression-observability.provider.ts and is wired up during backend startup. It is controlled via the N8N_EXPRESSION_ENGINE_OBSERVABILITY_* and N8N_EXPRESSION_ENGINE_TRACES_* environment variables (see below).
Main class for expression evaluation.
class ExpressionEvaluator {
constructor(config: EvaluatorConfig);
initialize(): Promise<void>;
acquire(owner: object): Promise<boolean>;
evaluate(expression: string, data: WorkflowData, caller: object, options?: EvaluateOptions): unknown;
release(owner: object): Promise<void>;
dispose(): Promise<void>;
isDisposed(): boolean;
}
Abstract interface for bridge implementations.
interface RuntimeBridge {
initialize(): Promise<void>;
execute(code: string, data: Record<string, unknown>): unknown;
dispose(): Promise<void>;
isDisposed(): boolean;
}
E() error handler for tournament-generated try-catch codeinterface EvaluatorConfig {
createBridge: () => RuntimeBridge; // required - factory, bridges are pooled
maxCodeCacheSize: number; // required - LRU size for tournament-transformed code
observability?: ObservabilityProvider; // optional - metrics/traces/logs provider
hooks?: TournamentHooks; // optional - AST security hooks for tournament
poolSize?: number; // optional - pre-warmed bridges, default 1
idleTimeoutMs?: number; // optional - scale pool to 0 after idle period
logger?: Logger; // optional - falls back to no-op
}
In n8n, the evaluator is configured via ExpressionEngineConfig (@n8n/config):
# Engine selection ('vm' is the default; 'legacy' opts out of isolation)
N8N_EXPRESSION_ENGINE=vm
# Isolate pool and code cache
N8N_EXPRESSION_ENGINE_POOL_SIZE=1
N8N_EXPRESSION_ENGINE_MAX_CODE_CACHE_SIZE=1024
N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT= # seconds; unset = pool never scales to 0
# Experimental
N8N_EXPRESSION_ENGINE_LAZY_ACQUIRE=false # create the isolate on the first evaluation that needs it
N8N_EXPRESSION_ENGINE_COMPILE_CACHE=false # reuse the V8 compile cache for the runtime bundle ('vm' only)
# Bridge limits
N8N_EXPRESSION_ENGINE_TIMEOUT=5000 # ms; positive integer
N8N_EXPRESSION_ENGINE_MEMORY_LIMIT=128 # MB; minimum 8
# Observability
N8N_EXPRESSION_ENGINE_OBSERVABILITY_ENABLED=true
N8N_EXPRESSION_ENGINE_TRACES_ENABLED=true
N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS=50
N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE=0.0
See packages/@n8n/config/src/configs/expression-engine.config.ts for the authoritative list and defaults.
# Install dependencies
pnpm install
# Build package
pnpm build
# Run tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Type check
pnpm typecheck
# Lint
pnpm lint
The package uses vitest for fast, isolated testing:
import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
describe('ExpressionEvaluator', () => {
it('evaluates simple expression', async () => {
const evaluator = new ExpressionEvaluator({
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
maxCodeCacheSize: 1024,
});
await evaluator.initialize();
const caller = {};
await evaluator.acquire(caller);
const result = evaluator.evaluate('{{ $json.value }}', { $json: { value: 42 } }, caller);
expect(result).toBe(42);
await evaluator.release(caller);
await evaluator.dispose();
});
});
Run tests:
pnpm test # Run all tests
pnpm test integration # Run integration tests only
The runtime uses several optimizations (implemented in PRs 2–4):
Performance characteristics:
The runtime enforces strict security at multiple layers (implemented in PRs 2–4):
ThisSanitizer rewrites $json → this.$json; PrototypeSanitizer wraps computed property access in this.__sanitize(key) to block prototype chain attacks; DollarSignValidator enforces correct $-variable usage__sanitize() inside the isolate blocks access to __proto__, constructor, prototype, and other dangerous properties at runtimeFuture security features (Phase 2+):
See the main n8n repository for contribution guidelines.
See LICENSE.md in the n8n repository root.
FAQs
Secure, isolated expression evaluation runtime for n8n
The npm package @n8n/expression-runtime receives a total of 119,805 weekly downloads. As such, @n8n/expression-runtime popularity was classified as popular.
We found that @n8n/expression-runtime demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 5 open source maintainers collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.