JS-Interpreter - 改变“this”上下文

JS-Interpreter - changing “this” context

JS-Interpreter 是一位有点知名的 JavaScript 口译员。它具有安全优势,因为它可以将您的代码与 document 完全隔离,并允许您检测无限循环和内存炸弹等攻击。这允许您安全地 运行 外部定义的代码。

我有一个对象,像这样说o

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

我希望能够通过 JS-Interpreter 运行 process 中的代码:

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

其中 interpretWithinContext 将使用第一个参数作为上下文创建解释器,即 o 变为 this,第二个参数是 [=36= 的代码行].在 运行 上面的代码之后,我希望 o 是:

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

也就是说,hiddenregex 现在已设置。

有谁知道这在 JS-Interpreter 中是否可行?

没试过JS-Interpreter。您可以使用 new Function()Function.prototype.call() 来达到要求

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

for (let i = 0; i < o.process.length; i++)
    console.log(new Function(`return ${o.process[i]}`).call(o));

您好,interpretWithinContext 看起来像那样吗?

let interpretWithinContext = (function(o, p){
   //in dunno for what you use p because all is on object o
   o.hidden = (o.hidden === null) ? false : o.hidden;
   o.regex = (o.regex === null) ? '/^[a-z]+$/i' : o.regex;

   console.log(o);
   return o;
});

https://codepen.io/anon/pen/oGwyra?editors=1111

我现在花了一段时间研究 JS-Interpreter,试图找出 from the source 如何将一个对象放入解释器的范围内,使其既可以读取又可以修改。

不幸的是,这个库的构建方式,所有有用的内部东西都被缩小了,所以我们不能真正利用内部东西,只是把一个对象放在里面。尝试添加 proxy object 也失败了,因为该对象没有以“正常”方式使用。

所以我最初的做法是退回到提供简单的实用函数来访问外部对象。这得到了库的完全支持,并且可能是与之交互的最安全方式。不过,它确实需要您更改 process 代码才能使用这些功能。但作为一个好处,它确实提供了一个非常干净的界面来与“外部世界”进行通信。您可以在以下隐藏代码段中找到解决方案:

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>


然而,在发布上述解决方案之后,我无法停止思考这个问题,所以我进行了更深入的挖掘。据我了解,方法 getPropertysetProperty 不仅用于设置初始沙箱范围,还用于解释代码。所以我们可以使用它为我们的对象创建类似代理的行为。

我的解决方案基于我发现 in an issue comment 通过修改 Interpreter 类型来执行此操作的代码。不幸的是,代码是用 CoffeeScript 编写的,并且还基于一些旧版本,所以我们不能完全按原样使用它。还有内部结构被缩小的问题,我们稍后会谈到。

总体思路是在范围内引入一个“连接对象”,我们将在 getPropertysetProperty 中将其作为特例处理,以映射到我们的实际对象。

但是为此,我们需要重写这两个方法,这是一个问题,因为它们被缩小并且收到了不同的内部名称。幸运的是,源码末尾包含以下内容:

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

所以即使缩小器破坏了右边的名字,它也不会触及左边的名字。这就是作者如何让特定功能可供 public 使用。但是我们要覆盖他们,所以我们不能只覆盖友好的名字,我们还需要替换缩小的副本!但是因为我们有办法访问这些函数,我们也可以搜索它们的任何其他具有损坏名称的副本。

这就是我在 patchInterpreter 开头的解决方案中所做的:定义我们将覆盖现有方法的新方法。然后,查找所有引用这些函数的名称(无论是否损坏),并将它们全部替换为新定义。

最后,在修补Interpreter之后,我们只需要将一个连接对象添加到作用域中即可。我们不能使用名称 this,因为它已经被使用过,但我们可以选择其他名称,例如 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>

就是这样!请注意,虽然新的实现已经适用于嵌套对象,但它可能不适用于每种类型。因此,您可能应该小心将哪种对象传递到沙箱中。仅使用基本或原始类型创建单独且明确安全的对象可能是个好主意。