0
\$\begingroup\$

I'm working on a modeling app and want to provide block scope for evaluation of user entered code. I'm assuming that the user is not malicious, but error prone, like all of us. At the same time, I want to avoid screwing up the app or the user's computer, so I want to prevent mucking with the global scope while still being able to access functionality. The following code is an attempt at doing that - it's based on the MDN docs on the with statement and Proxy object, combined with the scope objects in mathjs. FWIW, I'm intending to use this in a worker.

Simple tests tell me it prevents writes to globalThis. It doesn't prevent copying global objects into the local scope and then horking them, screwing up the model - but not my app. Can I do more?

Can I use the [Symbol.unscopables] magic to protect the scope from damage? If so, I'm not exactly clear how to use it.

Finally, is the penalty of using the Proxy object and with, more than walking parsed syntax trees like Mathjs and scraggy do?

function compileExpr(expr) { return new Function ("fscope", ` with(fscope) { return ${exp} } `); } function compileSnippet(snippet) { return new Function ("fscope", ` with(fscope) { ${snippet}; } `); } class MapScope { constructor (parent=null) { this.localMap = new Map() this.parentScope = parent; } has (key) { return this.localMap.has(key) ? true : this.parentScope?.has(key) ?? false; } get (key) { return this.localMap.get(key) ?? this.parentScope?.get(key) } set (key, value) { const iGetIt = ! this.parentScope?.has(key) || this.localMap.has(key); return iGetIt ? this.localMap.set(key, value) : this.parentScope.set(key, value); } keys () { if (this.parentScope) { return new Set([...this.localMap.keys(), ...this.parentScope.keys()]) } else { return this.localMap.keys() } } size() { return this.localMap.size();} forEach(cb, thisArg=this.localMap) { this.localMap.forEach(cb, thisArg); } delete (key) { // return false; return this.localMap.delete(key) } clear () { return this.localMap.clear() } toString () { return this.localMap.toString() } } var mapTraps = { // Lie and patch it up in get or set has(target, key) { console.log(`called: has(..., ${key}) === true`); return true; }, /* this allows us to create new variables in the local scope if they are not in the global scope or in a parent scope */ get(target,key,receiver) { if (!target.has(key)) { if (key in globalThis) { console.log(`called: get ${key.toString()} from globalThis`); return globalThis[key]; } else { return undefined; } } console.log(`called: get ${key.toString()} from target`); return target.get(key); }, // this will only set an underlying localMap set(target, key, val){ if (key in globalThis) { console.log(`called: set globalThis.${key}}. Nope!)`); return false; } console.log(`called: set(..., ${key.toString()})`); return target.set(key,val); }, } var mscope = new MapScope() var nscope = new MapScope(mscope) var rscope = new MapScope(nscope) var mproxy = new Proxy(mscope, mapTraps) var nproxy = new Proxy(nscope, mapTraps) var rproxy = new Proxy(rscope, mapTraps) 

and here are some simple tests and some examples of how it would be used in practice:

 with(mproxy) {Math = 0; Date=null}; // try to destroy Math & Date objects with(mproxy) {let t = Date.now(); x = t/100;} console.log(mscope); with (rproxy) {y=x*Math.random(); x += Math.random()}; console.log(rscope.localMap); console.log(mscope.localMap); mscope.clear(); rscope.clear(); var gVars = ` t0 = 0; x0 = Math.PI/4; p0 = {el: 0, pos: {x:0, y:0, z:0}}; rate = function (x,t1) { let delta = t1-t0; if (delta > 0) return (x - x0)/delta; return 0; } `; var lVars = `iteration = 0;`; var startExpr = ` t0 = Date.now(); x = x0; console.log(\`Starting time: \${t0}, iteration: \${iteration}\`); `; var startEachExpr = ` let t = Date.now(); iteration += 1; if (x < 5) { p0.pos.x += x; x += 2*Math.random(); } if (iteration > 0) { console.log(\`iteration: \${iteration}. rate is \${rate(x, t)}\`); } `; var globalInit = compileSnippet(gVars); var localInit = compileSnippet(lVars); var startFn = compileSnippet(startExpr); var startEachFn = compileSnippet(startEachExpr); globalInit(mproxy); localInit(nproxy); startFn(nproxy); console.log(JSON.stringify(mscope)); for (let i = 0; i< 3; i++) { startEachFn(rproxy); console.log(rscope); } 

If you've read this far, here's a link to the app, L-System Explorer, without these changes.

\$\endgroup\$

    0

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.