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
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:
- 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:
- 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:
- 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:
- 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:
- 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
|
- 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