Yimin Rong Yimin Rong - 1 year ago 92
Javascript Question

JS-Interpreter - changing “this” context

JS-Interpreter is a somewhat well-known JavaScript Interpreter. It has security advantages in that it can completely isolate your code from

document
and allows you to detect attacks such as infinite loops and memory bombs. This allows you to run externally defined code safely.

I have an object, say
o
like this:

let o = {
hidden: null,
regex: null,
process: [
"this.hidden = !this.visible;",
"this.regex = new RegExp(this.validate, 'i');"
],
visible: true,
validate: "^[a-z]+$"
};


I'd like to be able to run the code in
process
through JS-Interpreter:

for (let i = 0; i < o.process.length; i++)
interpretWithinContext(o, o.process[i]);


Where
interpretWithinContext
will create an interpreter using the first argument as the context, i.e.
o
becomes
this
, and the second argument is the line of code to run. After running the above code, I would expect
o
to be:

{
hidden: false,
regex: /^[a-z]+$/i,
process: [
"this.hidden = !this.visible;",
"this.regex = new RegExp(this.validate, 'i');"
],
visible: true,
validate: '^[a-z]+$'
}


That is,
hidden
and
regex
are now set.

Does anyone know if this is possible in JS-Interpreter?

Answer Source

I’ve spent a while messing around with the JS-Interpreter now, trying to figure out from the source how to place an object into the interpreter’s scope that can be both read and modified.

Unfortunately, the way this library is built, all the useful internal things are minified so we cannot really utilize the internal things and just put an object inside. Attempts to add a proxy object also failed failed since the object just wasn’t used in a “normal” way.

So my original approach to this was to just fall back to providing simple utility functions to access the outside object. This is fully supported by the library and probably the safest way of interacting with it. It does require you to change the process code though, in order to use those functions. But as a benefit, it does provide a very clean interface to communicate with “the outside world”. You can find the solution for this in the following hidden snippet:

function createInterpreter (dataObj) {
  function initialize (intp, scope) {
    intp.setProperty(scope, 'get', intp.createNativeFunction(function (prop) {
      return intp.nativeToPseudo(dataObj[prop]);
    }), intp.READONLY_DESCRIPTOR);
    intp.setProperty(scope, 'set', intp.createNativeFunction(function (prop, value) {
      dataObj[prop] = intp.pseudoToNative(value);
    }), intp.READONLY_DESCRIPTOR);
  }

  return function (code) {
    const interpreter = new Interpreter(code, initialize);
    interpreter.run();
    return interpreter.value;
  };
}


let o = {
  hidden: null,
  regex: null,
  process: [
    "set('hidden', !get('visible'));",
    "set('regex', new RegExp(get('validate'), 'i'));"
  ],
  visible: true,
  validate: "^[a-z]+$"
};

const interprete = createInterpreter(o);
for (const process of o.process) {
  interprete(process);
}

console.log(o.hidden); // false
console.log(o.regex); // /^[a-z]+$/i
<script src="https://neil.fraser.name/software/JS-Interpreter/acorn_interpreter.js"></script>


However, after posting above solution, I just couldn’t stop thinking about this, so I dug deeper. As I learned, the methods getProperty and setProperty are not just used to set up the initial sandbox scope, but also as the code is being interpreted. So we can use this to create a proxy-like behavior for our object.

My solution here is based on code I found in an issue comment about doing this by modifying the Interpreter type. Unfortunately, the code is written in CoffeeScript and also based on some older versions, so we cannot use it exactly as it is. There’s also still the problem of the internals being minified, which we’ll get to in a moment.

The overall idea is to introduce a “connected object” into the scope which we will handle as a special case inside the getProperty and setProperty to map to our actual object.

But for that, we need to overwrite those two methods which is a problem because they are minified and received different internal names. Fortunately, the end of the source contains the following:

// Preserve top-level API functions from being pruned/renamed by JS compilers.
// …
Interpreter.prototype['getProperty'] = Interpreter.prototype.getProperty;
Interpreter.prototype['setProperty'] = Interpreter.prototype.setProperty;

So even if a minifier mangles the names on the right, it won’t touch the ones on the left. So that’s how the author made particular functions available for public use. But we want to overwrite them, so we cannot just overwrite the friendly names, we also need to replace the minified copies! But since we have a way to access the functions, we can also search for any other copy of them with a mangled name.

So that’s what I’m doing in my solution at the beginning in patchInterpreter: Define the new methods we’ll overwrite the existing ones with. Then, look for all the names (mangled or not) that refer to those functions, and replace them all with the new definition.

In the end, after patching the Interpreter, we just need to add a connected object into the scope. We cannot use the name this since that’s already used, but we can just choose something else, for example o:

function patchInterpreter (Interpreter) {
  const originalGetProperty = Interpreter.prototype.getProperty;
  const originalSetProperty = Interpreter.prototype.setProperty;

  function newGetProperty(obj, name) {
    if (obj == null || !obj._connected) {
      return originalGetProperty.call(this, obj, name);
    }

    const value = obj._connected[name];
    if (typeof value === 'object') {
      // if the value is an object itself, create another connected object
      return this.createConnectedObject(value);
    }
    return value;
  }
  function newSetProperty(obj, name, value, opt_descriptor) {
    if (obj == null || !obj._connected) {
      return originalSetProperty.call(this, obj, name, value, opt_descriptor);
    }

    obj._connected[name] = this.pseudoToNative(value);
  }

  let getKeys = [];
  let setKeys = [];
  for (const key of Object.keys(Interpreter.prototype)) {
    if (Interpreter.prototype[key] === originalGetProperty) {
      getKeys.push(key);
    }
    if (Interpreter.prototype[key] === originalSetProperty) {
      setKeys.push(key);
    }
  }

  for (const key of getKeys) {
    Interpreter.prototype[key] = newGetProperty;
  }
  for (const key of setKeys) {
    Interpreter.prototype[key] = newSetProperty;
  }

  Interpreter.prototype.createConnectedObject = function (obj) {
    const connectedObject = this.createObject(this.OBJECT);
    connectedObject._connected = obj;
    return connectedObject;
  };
}
patchInterpreter(Interpreter);

// actual application code
function createInterpreter (dataObj) {
  function initialize (intp, scope) {
    // add a connected object for `dataObj`
    intp.setProperty(scope, 'o', intp.createConnectedObject(dataObj), intp.READONLY_DESCRIPTOR);
  }

  return function (code) {
    const interpreter = new Interpreter(code, initialize);
    interpreter.run();
    return interpreter.value;
  };
}


let o = {
  hidden: null,
  regex: null,
  process: [
    "o.hidden = !o.visible;",
    "o.regex = new RegExp(o.validate, 'i');"
  ],
  visible: true,
  validate: "^[a-z]+$"
};

const interprete = createInterpreter(o);
for (const process of o.process) {
  interprete(process);
}

console.log(o.hidden); // false
console.log(o.regex); // /^[a-z]+$/i
<script src="https://neil.fraser.name/software/JS-Interpreter/acorn_interpreter.js"></script>

And that’s it! Note that while that new implementation does already work with nested objects, it may not work with every type. So you should probably be careful what kind of objects you pass into the sandbox. It’s probably a good idea to create separate and explicitly safe objects with only basic or primitive types.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download