The eval Trick
Each direct eval creates a separate browser execution context, so the idea is to use it to break down scripts without the need of including additional scripts. The high-level idea is to split each script and subsequently each block recursively into blocks of eval calls smaller than 1kB (an arbitrary number), and somehow have the whole script behave as before.
- Test run in the browser.
- Crawl top 1000 websites and analyze the logs.
Why it should work
Direct eval calls are executed in the same scope as the caller, with read-write access to all of the surrounding variables.
Challenges and workarounds
In reality, the eval trick is a giant hack due to the quirks of eval.
Hoisting: some constructs behave as if they are evaluated first before the script is run.
Top-level functions.
We simply
evalthem first.Variable declarations. Functions cannot capture variables declared in adjacent
evals, and would capture the variable in the outer scope with the same name instead, causing errors. Variables created inevaldo not leak out unless they are declared withvarin non-strict mode.We identify all variable declarations in the current scope, declare all of them first using
varorlet, then make sure they are not declared again in theevalcalls on the same level.(Deferred) Some assignments do not have a single variable identifier on their left-hand side (LHS), such as destructuring. The LHS can contain arbitrary expressions.
We currently stick
varto their beginning so most of them work, but the assigned variables do not leak out. This is usually fine because most such assignments are for local variables anyway.
returnstatements cannot return from insideeval.For each
evalblock that containsreturnstatements not in nested functions or classes, we wrap it in an immediately invoked function expression (IIFE) so that thereturns are valid. We call the IIFE with.call(this)to preservethis. We then check the return value ofevaland return early it if it is notundefined.Functions declared in IIFEs do not leak out.
We declare the function identifiers as variables first, then convert all function declarations to assignments of function expressions to those variables.
(Deferred) This seems to break a few scripts, especially when they call functions defined in other scripts.
No idea for solutions yet because the error messages did not reveal the cause.
importstatements are not allowed insideeval.We put them at the top of the script.
exportstatements are not allowed insideeval.We keep these scripts as is.
break,continueandyieldstatements may not be able to reach the correct outer scopes insideeval.We do not rewrite with
evalin loops or generators until we hit a function or class boundary.awaitdoes not work insideeval.We
awaiton theevaland use an async IIFE if an IIFE is used.Bloat: deeply nested
evals cause mountains of backslashes.We use
String.rawto avoid escaping backslashes and nest nestedevalcalls in functions to escape the backticks and${at runtime.Browser crash with stack size exceeded when nesting
evalto over depth 8.We limit the depth of
evalnesting to 8 and inline the deeper blocks.
Inherent limitations
- Performance:
evaldisables the JIT and forces slow variable lookups. - Integrity: e.g., if the website checks the checksum of the script, it will fail.