Module: lib/core/expression/evaluator

Evaluates a parsed expression against a scope.

This is where the security boundary actually is

The old sandbox tried to be a boundary from outside the evaluation: it wrapped values that reached an expression through the scope, and it grepped the source text for constructor, __proto__ and prototype. Neither survives contact with the language.

({})['const'+'ructor']['const'+'ructor']('return 1')()

The object literal is created by the engine inside the expression, so it never passes through the scope and is never wrapped; and the property name is assembled at runtime, so no amount of reading the source finds it. Both escapes are consequences of handing the expression to new Function and inspecting from the outside.

Here, nothing is handed over. Every property read goes through readMember with the key already resolved to a string, so x.constructor and x['const'+'ructor'] arrive at the same check as the same value. Every call goes through invoke. There is no path to a value the evaluator did not itself produce.

What is guaranteed, and what is not

Guaranteed: an expression cannot reach the Function constructor, cannot read or write __proto__ / constructor / prototype however the key is spelled, cannot reach a built-in prototype object, and cannot name a global outside ALLOWED_GLOBALS. There is no eval and no new Function, so a page carrying only Avenx expressions does not need 'unsafe-eval'.

Not guaranteed: this is not an isolation boundary against hostile expression source*. An expression can still call any function the scope legitimately exposes, and a bridge action can do whatever its own JavaScript does. The boundary protects the runtime from accidents and from reaching outside the declared scope; it does not make it safe to evaluate expressions written by an untrusted party. Anything stronger would need a separate realm, and claiming it without one is how the previous sandbox came to be believed.

Determinism

Global resolution goes through the tracer's substitution point, exactly as the previous sandbox did, so a recorded session still observes and replays the non-deterministic values (Date, Math.random) an expression sees. There is a single evaluation choke point here, which is what Trace needs.

Source:

Classes

Frame

Members

(static, constant) ALLOWED_GLOBALS :Set:.<string:>

Globals an expression may name.

Deliberately identical to the set the previous sandbox allowed, so migrating an application changes nothing about which globals its templates can see.

Type:
  • Set:.<string:>
Source:

(inner, constant) FORBIDDEN_KEYS :Set:.<string:>

Property names an expression may never read or write.

Checked against the resolved key, so a computed access spelled x['const' + 'ructor'] is rejected on the same terms as x.constructor.

Type:
  • Set:.<string:>
Source:

(inner, constant) FUNCTION_CONSTRUCTORS :Set:.<function()>

The dynamic-code constructors.

Reaching any of these would be arbitrary code execution, so they are refused as values however they were obtained.

Type:
  • Set:.<function()>
Source:

(inner, constant) PROTECTED_PROTOTYPES :Set:.<object:>

Built-in prototypes shared by every object in the realm.

Handing one to an expression would let it mutate state shared with the host page, so they are refused as values rather than by enumerating every mutating method on them.

Type:
  • Set:.<object:>
Source:

(inner, constant) RESTRICTED_GLOBALS :Set:.<string:>

Globals an expression may never name, whether or not the host defines them.

Listed explicitly rather than inferred from in globalThis, because the diagnostic has to be the same in every environment: localStorage is restricted in a browser, in happy-dom and in bare Node, and a developer reading AVX_R15 should not get a different answer depending on where the expression happened to run.

Type:
  • Set:.<string:>
Source:

Methods

(static) evaluate(node, scope, optionsopt) → {any}

Evaluates a parsed expression.

Parameters:
Name Type Attributes Description
node object

The AST root.

scope object

The evaluation scope. Reads and writes of names the scope binds go through it, which is what keeps dependency tracking working.

options object <optional>

Evaluation options.

Properties
Name Type Attributes Description
frame Frame <optional>

The enclosing binding frame.

Source:
Returns:

The expression's value.

Type
any

(static) invoke(fn, thisArg, args, description) → {any}

Calls a function on behalf of an expression.

Parameters:
Name Type Description
fn any

The callee.

thisArg any

The receiver.

args Array:.<any:>

The arguments.

description string

How the callee was written, for the error.

Source:
Returns:

The result.

Type
any

(static) readMember(object, key, optional) → {any}

Reads a property, with the key already resolved.

This is the single gate the old sandbox lacked. Because the key arrives as a value rather than as source text, there is no spelling of it that avoids the check.

Parameters:
Name Type Description
object any

The object to read from.

key any

The resolved property key.

optional boolean

Whether the access used ?..

Source:
Returns:

The property value.

Type
any

(static) writeMember(object, key, value) → {any}

Writes a property, with the key already resolved.

Parameters:
Name Type Description
object any

The object to write to.

key any

The resolved property key.

value any

The value to assign.

Source:
Returns:

The assigned value.

Type
any

(inner) applyBinary(operator, left, right) → {any}

Applies a binary operator.

Parameters:
Name Type Description
operator string

The operator.

left any

Left operand.

right any

Right operand.

Source:
Returns:

The result.

Type
any

(inner) applyCompound(operator, current, operand) → {any}

Computes the value an assignment operator produces.

Parameters:
Name Type Description
operator string

The assignment operator.

current any

The current value.

operand any

The right-hand value.

Source:
Returns:

The value to store.

Type
any

(inner) describe(node) → {string}

Describes a node for an error message.

Parameters:
Name Type Description
node object

The node.

Source:
Returns:

A short human-readable description.

Type
string

(inner) evalNode(node, scope, frame) → {any}

Evaluates one AST node.

Parameters:
Name Type Description
node object

The node.

scope object

The evaluation scope.

frame Frame | null

The enclosing binding frame.

Source:
Returns:

The node's value.

Type
any

(inner) guardValue(value) → {any}

Refuses a value that must never reach an expression.

Parameters:
Name Type Description
value any

The value about to be returned.

Source:
Returns:

The value, when it is allowed.

Type
any

(inner) isKnownGlobal(name) → {boolean}

Whether a name exists on the host global object.

Parameters:
Name Type Description
name string

The identifier.

Source:
Returns:

True when the host defines it.

Type
boolean

(inner) readIdentifier(name, scope, frame) → {any}

Resolves an identifier to a value.

Parameters:
Name Type Description
name string

The identifier.

scope object

The evaluation scope.

frame Frame | null

The enclosing binding frame.

Source:
Returns:

The bound value.

Type
any

(inner) refuse(message)

Raises a sandbox violation.

Parameters:
Name Type Description
message string

What was refused.

Source:
Throws:

Always.

Type
AvenxError

(inner) writeIdentifier(name, value, scope, frame) → {any}

Assigns to an identifier.

Parameters:
Name Type Description
name string

The identifier.

value any

The value to assign.

scope object

The evaluation scope.

frame Frame | null

The enclosing binding frame.

Source:
Returns:

The assigned value.

Type
any