Stage 1 Draft / October 10, 2019

Dynamic Code Brand Checks

See also the explainer.

Background

The Trusted Types proposal (explainer for TC39) seeks to double-check risky operations like code loading by requiring that code portions have a runtime type that indicates that they have been explicitly trusted. Specifically, when Trusted Types is turned on, it would like to ensure that arguments to eval(x) and new Function(x) are (or are convertible to) TrustedScript.

Historical Note

After feedback from TC39, this proposal was merged from two stage-0 proposals:

Problem 1: %eval% does not accept objects in lieu of strings for code

PerformEval early outs when its argument is not a string.
  1. If Type(x) is not String, return x.
This proposal relaxes the "Type(x) is String" requirement for inputs to eval to enable restricting dynamic code loading to values that have been attested to previously.

Problem 2: host callout does not receive type information

Currently, the information available to the check is per-realm:
  1. Perform ? HostEnsureCanCompileStrings(callerRealm, calleeRealm).
This proposal aims to provide additional context to HostEnsureCanCompileStrings, and reorder the steps in CreateDynamicFunction so that HostEnsureCanCompilerStrings receives runtime type information. The production to use to parse the source text. The value of any parameters and source text before coercion to strings. A bit to distinguish between indirect and direct eval.

Problem 3: host callout cannot adjust values

In Trusted Types, the default policy's createScript callback returns a value that is used in place of the input. For example, if the default policy's createScript callback returns "output" given "input" then eval("input") would load and run a ScriptBody parsed from the source text output.

<meta http-equiv="Content-Security-Policy" content="trusted-types default" />
<script>
// Define a default policy that maps the source text `input` to `output`.
TrustedTypes.createPolicy(
  'default',
  {
    createScript(code) {
      if (code === 'input') { return 'output'; }
      throw new Error('blocked script execution');
    },
  });

globalThis.input = 1;
globalThis.output = 2;
// The source text loaded is `output`
eval('input') === globalThis.output;  // true
</script>
This proposal adjusts callers of the callback to expect a result and to use it in place of the inputs.

Changes to HostEnsureCanCompileStrings

1HostEnsureCanCompileStrings HostBeforeCompileValue ( callerRealm, calleeRealm, goal, args)

HostEnsureCanCompileStrings HostBeforeCompileValue is an implementation-defined abstract operation that allows host environments to block certain ECMAScript functions which allow developers to compile strings into ECMAScript code.

An implementation of HostEnsureCanCompileStrings HostBeforeCompileValue may complete normally or abruptly. Any abrupt completions will be propagated to its callers. The default implementation of HostEnsureCanCompileStrings HostBeforeCompileValue is to unconditionally return an empty a normal completion with a value of args.

args must be a list of values.

Any normal completion value from HostBeforeCompileValue must be a list of values.

Any normal completion value from HostBeforeCompileValue should be used by callers in place of the input args.

Note 1
For eval args will have a single element, the source to evaluate. For MakeDynamicFunction it may have any number of elements that are joined on comma to form source text for FormalParameters followed by a body.
Note 2
goal is the production which will be used to parse the last element of args. For example, if called via %Function% it might be the grammar symbol FunctionBody[~Yield, ~Await] and from %eval% it might be the grammar symbol Script.

Implementations that stringify any element i of args where i ≠ 0 must stringify args[i-1] before stringifying args[i].

Implementations that stringify any element of args should return the stringified result in place of that element where that element informs part of the output.

Note 3
This avoids visible side-effects due to multiple stringification of user-defined objects as in:
new Function(
  {
    toString() {
      console.log('parameter stringified');
      return 'x, y';
    }
  },
  {
    toString() {
      console.log('body stringified');
      return 'x + y';
    }
  });

Implementations must return a single element list when args has a single element and goal is ScriptBody.

Note 4
This avoids complicating PerformEval.

Changes to CreateDynamicFunction

CreateDynamicFunction now waits until after it figures out what kind of function it is creating and uses the result of the adjusted host callout.

2Runtime Semantics: CreateDynamicFunction ( constructor, newTarget, kind, args )

The abstract operation CreateDynamicFunction is called with arguments constructor, newTarget, kind, and args. constructor is the constructor function that is performing this action, newTarget is the constructor that new was initially applied to, kind is either "normal", "generator", "async", or "async generator", and args is a List containing the actual argument values that were passed to constructor. The following steps are taken:

  1. Assert: The execution context stack has at least two elements.
  2. Let callerContext be the second to top element of the execution context stack.
  3. Let callerRealm be callerContext's Realm.
  4. Let calleeRealm be the current Realm Record.
  5. Perform ? HostEnsureCanCompileStrings(callerRealm, calleeRealm).
  6. If newTarget is undefined, set newTarget to constructor.
  7. If kind is "normal", then
    1. Let goal be the grammar symbol FunctionBody[~Yield, ~Await].
    2. Let parameterGoal be the grammar symbol FormalParameters[~Yield, ~Await].
    3. Let fallbackProto be "%FunctionPrototype%".
  8. Else if kind is "generator", then
    1. Let goal be the grammar symbol GeneratorBody.
    2. Let parameterGoal be the grammar symbol FormalParameters[+Yield, ~Await].
    3. Let fallbackProto be "%Generator%".
  9. Else if kind is "async", then
    1. Let goal be the grammar symbol AsyncFunctionBody.
    2. Let parameterGoal be the grammar symbol FormalParameters[~Yield, +Await].
    3. Let fallbackProto be "%AsyncFunctionPrototype%".
  10. Else,
    1. Assert: kind is "async generator".
    2. Let goal be the grammar symbol AsyncGeneratorBody.
    3. Let parameterGoal be the grammar symbol FormalParameters[+Yield, +Await].
    4. Let fallbackProto be "%AsyncGenerator%".
  11. Set args to ! HostBeforeCompileValue(callerRealm, calleeRealm, goal, args).
  12. Let argCount be the number of elements in args.
  13. ...

Changes to PerformEval

PerformEval now:

3Runtime Semantics: PerformEval ( x, callerRealm, strictCaller, direct )

The abstract operation PerformEval with arguments x, callerRealm, strictCaller, and direct performs the following steps:

  1. Assert: If direct is false, then strictCaller is also false.
  2. If Type(x) is not String IsCodeLike(x) is false, return x.
  3. Let evalRealm be the current Realm Record.
  4. Let args be a list containing only x.
  5. Perform ? HostEnsureCanCompileStrings(callerRealm, calleeRealm).
    Set args to ! HostBeforeCompileValue(callerRealm, calleeRealm, Script, args).
  6. Set x to ? Get(args, 0).
  7. ...

3.1Runtime Semantics: IsCodeLike ( x )

The abstract operation IsCodeLike with argument x performs steps TBD (see options below).

Option 1 - Internal slot [[CodeLike]]

We could use an internal slot to distinguish code-like objects.

With this approach, existing programs that assume that typeof x === 'string' || eval(x) === x will continue to have the same semantics since there's no existing APIs that create non-string code-like values.

There is no way for user code to define code-like values unless we additionally provide APIs that affect the [[CodeLike]] internal slot.

Proxies over code-like values are not themselves code-like.

4Runtime Semantics: IsCodeLike ( x )

The abstract operation IsCodeLike with argument x performs the following steps:

  1. If Type(x) is String, return true.
  2. If Type(x) is Object, then
    1. If x has a [[CodeLike]] internal slot, then return true.
  3. Return false.

Option 2 - Well known symbol

We could use a new well-known symbol to distinguish code-like objects.

With this approach, existing programs that assume that typeof x === 'string' || eval(x) === x may have different semantics if they use Symbol[key] or equivalent to access the new well-known symbol and then attach that to objects.

User code may define code-like values.

Proxies over code-like values are themselves code-like.

5Well-Known Symbols

Table 1: Well-known Symbols
Specification Name [[Description]] Value and Purpose
@@evalable "Symbol.evalable" Boolean property that indicates whether the object may be interpreted as code by eval.

6Runtime Semantics: IsCodeLike ( x )

The abstract operation IsCodeLike with argument x performs the following steps:

  1. If Type(x) is String, return true.
  2. If Type(x) is Object, then
    1. Let evalable be ? Get(x, @@evalable).
    2. Return ToBoolean(evalable).
  3. Return false.

ACopyright & Software License

Copyright Notice

© 2019 Mike Samuel

Software License

All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.