Proposal: Reflect.applyLegacy

Background

With ESM default strict and "use strict" directive, there is one particular JS feature we lost in the process, a feature that requires mandatory usage of Function(...) evaluation to be able to escape from strict semantics of the language.

Mavo, Alpine.js, Vue.js, tag-params and many others, use evaluation mostly to be able to use the with(...) statement, or to simply retrieve the global context, like it's done in Angular.

Proposal

Allow escaping from strict semantics through Reflect.applyLegacy(target, thisArgument, argumentsList) so that:

Reflect.applyLegacy(
  function() { return this },
  void 0,
  []
) === globalThis; 
// true in every env

Reflect.applyLegacy(
  function() { with(this) return name; },
  {name: 'legacy'},
  []
) === 'legacy';
// true even in strict environments

In short, the idea here is to have some sort of "use sloppy" directive enabled through a very explicit method name that also suggests it's a bad idea, slow, or unpredictable, yet it completely remove from various code bases the need to have new Function and CSP or security concerns all together.

Thank you!

P.S. no strong feelings around the name, it just felt aligned with what we have already as Reflect method and intent, but applyDeprecated or applySloppy or applyNonStrict would work either ways, it's the feature that I'm interested in, not really its name.

2 Likes

What wouldn't this solve?

It's fair to point out that without string composition, as example grabbing templates from files or composing at runtime the code that would end up using the with statement (or other features), bringing back legacy (hopefully sporadically) might not fully solve the need those libraries have around new Function usage, beside having with feature. Somebody mentioned a new SafeFunction that also escape the strict directive would be the ideal solution for all cases, but I think evaluation can't really have compromises at CSP level, so at least, when just with is needed, let's consider allowing devs to use that feature in particular.

Not to derail the entire thread, but Reflect is only ever for Proxy traps, so that wouldn't be a viable location for something like this.

1 Like

I don't really see the problem. All the examples you linked are doing some sort of templating, where Function() is standard anyway. That they get to use with inside those template codes is just a bonus.

This is not really possible. Directives provide context for the parser, they change how the function is defined, at declaration site. The strictness of a function does not depend on where or how it is called, but only on where it is defined. applyLegacy should not change that, and cannot change that.

Notice that

function demo() { "use strict"; with(this) return name; }

is simply a syntax error. A syntax error that is happening way before

Reflect.applyLegacy(demo, {name: 'legacy'}, []) === 'legacy';

Both Reflect and static analysis are very valid points, however this kinda closes this proposal: “Function is the way to solve templating and with there is a bonus”.
I’ve written myself that use case wouldn’t indeed be solved so maybe this was more some sort of brain fart that won’t bring much to that plate, likely confuse developers instead.

Is the ability to have a SafeFunction where evaluation, as example, won’t accept inner eval invokes or can analyze a function is not dangerous to evaluate something possible?

Anyway, closing this as it can’t go forward due mentioned restrictions.

It all depends on what you consider "safe". Even with nested evaluation disabled, I wouldn't consider this sort of evaluation safe if it can access and / or mutate the global object which posseses powerful capabilities.

However making eval safe (including nested eval) is the goal of SES / Hardened JS. The proposal itself is very stale, but the shim is not (and actively used in production. The Compartment part is more up to date, and contains the building blocks for creating evaluators with a different global scope, which should alleviate most needs for with. To prevent mutation of the shared intrinsics, you still need lockdown which is specific to SES.

Another approach would be the new ShadowRealm API, since you get to create a new set of intrinsics and evaluate code within that context.

coming from this current situation you were also involved, I went ahead and tried to think about a way to cache a well known structure (of tokens) once and be able to create clones on demand in the fastest possible way.

Here there's a benchmark, and (not surprisingly) the result is that Function instrumented in a super-safe way, basically putting a stringified JSON in it and returning it each time, smokes any other standard solution ... plus, it's not less safer by any mean.

Like it is for any proposal, I don't feel like exploring this approach, but also I don't think it would be faster than any solutions explored in the mentioned benchmark. Surely not faster to initialize, also considering that's an async by design solution, hence it won't work on the fly for synchronous operations like I need to have/do in my case.

I'm not sure I follow. It seems in this case you trust the content of the Function's body to evaluate since you generate it yourself. The thing your evaluation seem to rely on is that it won't reach to the global scope at all, and won't attempt to mutate intrinsics through syntactic access. A compartment (or more precisely an evaluator with custom global, aka layer 3 of compartment proposal) plus frozen intrinsics would guarantee you this. An alternative to the frozen intrinsics would be to somehow execute the resulting function in "pure" mode since it's not supposed mutate anything.

Btw, the benchmark is not comparing apples to apples since the function case re-evaluates Math.random() for every invocation, which is where most of the performance difference with Function comes from.

In this case the callable boundary of ShadowRealm would prevent you from sharing the object back to your own realm, so if performance is your motivation, that would definitely not be the right approach here.

If you had the opposite problem, aka make sure you can compare 2 structures by value / content instead of by reference, then I'd say the upcoming Records and Tuples proposal should be useful, but we're definitely deviating from the topic of safe evaluation.

I've represented in that benchmark what I would do (more or less) in the real world, where I would pass those Math.random() operations each time but fair enough, it's just that for a bench I didn't feel like wasting too much time. Basically the function() is there for comparison reason and nothing else, as it's impossible for me to define such function out of my own code because if I create a token tree, I can't just return that, I need to clone it iwth its current values as per discussion happening elsewhere.

With Function though, I can pass as argument an instrumented object such as {Component, v0, v1, v2, v3} and create programmatically that JSON like object to assign {type: COMPONENT, value: Component, children: [{type: INTERPOLATION, value: v0}]} and so on, so that each unique template represents a unique instrumented function and mapping to create the object to pass along.

It's true though that Math.random() should be out of the equation, but once I've optimized for the bare function result, I am still trapped by the inability to create that function via code, because of CSP and security gotchas around Function. A SafeFunction that simply understands there is no reach to any outer scope would work miles for mine, and every other use case I know to date.

My thoughts too ... so I am trapped with no solutions, even if there's nothing intrinsically unsafe in any of my intents, which is why I've opened this issue in the first place, although I don't even care anymore about with operations, if I could have a SafeFunction around.

can I have solutions today though, or with what's parsing Web, servers, and IoT in terms of engines? That's why I am finding alternatives, yet I don't like any of them.

From a JavaScript spec perspective, Function is the correct answer for you here. The fact that CSP gets in the way is a problem specific to the Web platform. You could probably mitigate it with trusted types, which unfortunately is only supported by chromium browsers.

Even with compartments and custom evaluators, I would expect web browsers to not relax CSP (the same way CSP is not relaxed in ShadowRealm), as they view arbitrary JS code execution as a security risk that can only be mitigated by process based isolation (cross origin policy).

Don't shoot the messenger, I'm just reporting what the web platform consensus seem to be (some JS delegates like myself disagree that same origin isolation is the only "security" mechanism).

first time I heard of it ... not the first time I hear something is chromium only though :-(

apologies if you felt that way, I guess (like I've mentioned elsewhere) I've reached a point the platform is asking me to use Function and while I really don't want to, I might just do that for performance and logic sake ... it's scripting, after all, and let the scripting be!

If I had alternatives, I would use these ... I don't, so something is missing, and realms are not the answer. Vue, among many other frameworks, use Function or even AsyncFunction in the wild and are super successfull, but if sites are forced to relax CSP due de-facto standard libraries, are these standards really helping? I guess a topic for a different thread though, and thanks for your hints and details I wasn't aware of, a lot of TIL today because of you +1