mikesamuel / evalable

Relax the requirement that the argument to eval be a string in a non-breaking way
MIT License
3 stars 0 forks source link

This proposal has merged into dynamic-code-brand-checks

Relax the requirement that the argument to eval be a string in a non-breaking way.

Background

Currently, typeof x === 'string' || eval(x) === x.

For example:

console.log(eval(123) === 123);

const array = [ 'alert(1)' ];
console.log(eval(array) === array);  // Does not alert

const touchy = { toString() { throw new Error(); } };
console.log(eval(touchy) === touchy);  // Does not throw.

This follows from step 2 of the definition of PerformEval:

Runtime Semantics: PerformEval ( x, evalRealm, strictCaller, direct )

The abstract operation PerformEval with arguments *x*, *evalRealm*, *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, return x.

Goal

Encourage source to sink checking of arguments to eval to reduce the risk of XSS.

Source to sink checking means we do something at the source of a value so that, when the value reaches a "sensitive sink" (like eval), we can double-check that it is safe.

The Trusted Types proposal brings source-to-sink checks to browser built-ins like .innerHTML.

  1. Introduce a number of types that correspond to the XSS sinks we wish to protect.

TrustedScript: This type would be used to represent a trusted JavaScript code block i.e. something that is trusted by the author to be executed by ... passing to an eval function family.

  1. Enumerate all the XSS sinks we wish to protect. ...

  2. Introduce a mechanism for disabling the raw string version of each of the sinks identified. ...


As valid trusted type objects must originate from a policy, those policies alone form the trusted codebase in regards to DOM XSS.

So by double-checking at sinks like eval we can ensure that only carefully reviewed code that is the source of TrustedScript values will load alongside trusted code.

This won't work today because, as explained above, eval(myTrustedScript) does not actually evaluated the textual content of TrustedScript values.

The Larger Context

This proposal is useful in the context of other planned changes:

  1. The Trusted Types proposal as explained.
  2. A proposed tweak to the HostEnsureCanCompileStrings that would allow browsers to take more context into account when deciding whether to allow a particular call to eval.

Backwards compatibility constraints

To avoid breaking the web, eval(x) should do nothing different when x is a non-string value that existing applications might create.

This should be the case regardless of whether HostEnsureCanCompileStrings says yes or no as demonstrated in node:

$ npx node@10 --disallow_code_generation_from_strings -e 'console.log(eval(() => {}))'
[Function]

$ npx node@10 --disallow_code_generation_from_strings -e 'console.log(eval("() => {}"))'
[eval]:1
console.log(eval("() => {}"))
        ^

EvalError: Code generation from strings disallowed for this context

When eval is passed a non-string value, even though the Host callback rejects all attempts to eval, it doesn't throw when given the value () => {}, but does when given the string "() => {}".

Possible solutions

You can browse the ecmarkup output or browse the source.

All the approaches below do the following

Internal slot

Pros

Well-known Symbol

From ecma262 issue #938:

Include the string to be compiled in the call to

HostEnsureCanCompileStrings Perhaps we could tweak the eval algo so that if the input is a string or has a particular symbol property then it is stringified to avoid breaking code that depends on the current behavior where eval is identity except for strings.

Pros

Testing

TODO: Updates to

Related

https://github.com/tc39/ecma262/issues/1495