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,第二个参数是要运行的代码行。运行以上代码后,我希望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,试图弄清楚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不仅用于设置初始沙箱范围,而且还在解释代码时使用。因此,我们可以使用它为我们的对象创建类似代理的行为。

我在这里的解决方案基于我通过修改Interpreter类型发现in an issue comment的代码。不幸的是,该代码是用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;

因此,即使一个缩小器破坏了右侧的名称,也不会碰触左侧的名称。这就是作者将特定功能提供给公众使用的方式。但是我们要覆盖它们,所以我们不能只覆盖友好名称,还需要替换缩小的副本!但是,由于我们有访问功能的方法,因此我们也可以搜索名称错误的其他任何副本。

因此,这就是我在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>


就是这样!请注意,尽管该新实现已对嵌套对象起作用,但可能并非对每种类型都起作用。因此,您应该小心将哪种对象传递到沙箱中。最好只创建基本或原始类型的单独且明确安全的对象,这是一个好主意。

09-27 13:20