Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require does not work as expected within vm #2503

Closed
reggi opened this issue Aug 23, 2015 · 10 comments
Closed

Require does not work as expected within vm #2503

reggi opened this issue Aug 23, 2015 · 10 comments
Labels
question Issues that look for answers. vm Issues and PRs related to the vm subsystem.

Comments

@reggi
Copy link
Contributor

reggi commented Aug 23, 2015

I decided to port this over from stackoverflow because it's so specific to node & vm.

The code below uses the native node vm library, which allows you to evaluate javascript strings in different contexts.

The particular code within example.js there's a javascript string that adds a property .marker with the value true to the Array global variable, then requires a file global.js (seen below), then logs Array.marker. The code below logs true.

var vm = require('vm')

var code = [
  'Array.marker = true',
  "require('./global.js')",
  'console.log(Array.marker)', // => true
].join('\n')

var script = new vm.Script(code, { filename: 'example.js' })

script.runInNewContext({
  'require': require,
  'console': console
})

Below is the contents of global.js, which is a simple module that changes the value of Array.marker to false.

var hi = function () {
  Array.marker = false
}
module.exports = hi()

What should be happening here is that the code within the vm should be setting Array.marker to true then the global.js module should be changing it the value to false and it should log false.

If you go ahead and run the contents of the javascript string outside of vm, in it's own file, you will get the expected result, Array.marker will be equal to false.

Array.marker = true
require('./global.js')
console.log(Array.marker) // => false

The question is: Why doesn't Array.marker get updated to the correct value (true)? How can I allow the value of Array.marker to get updated from within the global.js module?

Is this an issue with the native node.js vm module? Or is this not supposed to be possible? Or are my settings off?

@brendanashworth brendanashworth added vm Issues and PRs related to the vm subsystem. feature request Issues that request new features to be added to Node.js. question Issues that look for answers. and removed feature request Issues that request new features to be added to Node.js. labels Aug 23, 2015
@bnoordhuis
Copy link
Member

Each VM context gets its own set of global objects, including the Array object. In other words:

> vm.runInNewContext('Array') === Array
false

If you want to modify the Array object from the current context, pass it to the new context:

> vm.runInNewContext('Array', { Array: Array }) === Array
true

But note the following caveat:

> vm.runInNewContext('new Array', { Array: Array }) instanceof Array
true
> vm.runInNewContext('[]', { Array: Array }) instanceof Array
false

Hope that helps.

@reggi
Copy link
Contributor Author

reggi commented Aug 23, 2015

Hey @bnoordhuis thanks for the post, and your time. I really appreciate it. All of what you said in your comment I completely understand, and makes total sense.

I think the bug goes a little deeper. It has more to do with the behavior of the imported require then it does with vm, to demonstrate that a bit more accurately.

Here I change the value of Array.marker from within a new vm context, and you can see it has no effect on the global Array.marker after the vm runs. This is expected behavior because I didn't pass anything into vm.

var vm = require('vm')
var code = [
  'Array.marker = true',
].join('\n')
vm.runInNewContext(code)
console.log(Array.marker) // => undefined [expected]

However when I bring require into the vm something very unexpected happens. The code required maintains the scope of the parent context. So with the given code, you get unexpected results. The global within the vm script Array.marker is not changed due to the require call, and you can see the require call is editing the parent scope by setting Array.marker.

var vm = require('vm')
var code = [
  'Array.marker = true',
  "require(\'./global.js\')",
  'console.log(Array.marker)' // => true [unexpected]
].join('\n')
vm.runInNewContext(code, {
  require: require,
  console: console
})
console.log(Array.marker) // => false [unexpected]

The real question is how can instantiate a new instance of require within a vm rather then importing the parent's? Keeping it localized to the vm rather then allowing it to leak into the parent.

@reggi reggi changed the title Cannot change global variable from within module within vm Require does not work as expected within vm Aug 23, 2015
@reggi
Copy link
Contributor Author

reggi commented Aug 23, 2015

The only trick I've found to get require to work the way I want within the vm code is if I create a file with the contents of the code, and require that file.

usage.js

Array.marker = true
require('./global.js')
console.log(Array.marker) // => false [expected]

Then I can require the file and the global scope within the code works as expected.

var vm = require('vm')
var code = [
  "require('usage.js')"
].join('\n')
vm.runInNewContext(code, {
  require: require,
  console: console
})
console.log(Array.marker) // => false [unexpected]

However this still leaks out Array.marker.

Is there any way to load a string as-if it's being required?

@reggi
Copy link
Contributor Author

reggi commented Aug 23, 2015

Here's a proof-of-concept, that takes the code and will write it to a file and the vm requires the newly created file.

var path = require('path')
var os = require('os')
var crypto = require('crypto')
var fs = require('fs')
var vm = require('vm')

var globalPath = path.resolve('./global.js')

var code = [
  "Array.marker = true",
  "require('"+globalPath+"')",
  "console.log(Array.marker)", // => true [expected]
].join('\n')

var getHash = function (content) {
  var digest = crypto.createHash('md5')
  return digest.update(content).digest('hex')
}

function encapsulate (code, fileName) {

  // hash the name to ensure files are saved in secure location
  // prevent ../../filename.js
  var hashName = getHash(fileName) + '.js'
  var filePath = path.join(os.tmpdir(), hashName)

  fs.writeFileSync(filePath, code)

  code = [
    "require('" + filePath + "')"
  ].join('\n')

  var value = vm.runInNewContext(code, {
    require: require,
    console: console
  })

  fs.unlinkSync(filePath)

  return value
}

encapsulate(code, 'hi.js')

console.log(Array.marker) // => false [unexpected]

@bnoordhuis
Copy link
Member

However when I bring require into the vm something very unexpected happens. The code required maintains the scope of the parent context.

That's the expected behavior and it makes sense when you realize that VM contexts are used to implement iframes in Chromium. Calling a function runs it in the context that created it.

@reggi
Copy link
Contributor Author

reggi commented Aug 23, 2015

@bnoordhuis What your telling me is great info and context. Yes vm is based on v8 / chrome but require is a Node.js construct. Node adds require to a file's scope. I need a way of treating the vm as a file and allowing it to require within it's own scope, localized to the vm and not the parent. Is there a drop in replacement for require?

I'm not alone in wanting to use require within a vm. Here @isaacs adds require to the sandbox as well.

The purpose of the vm module is to create a new v8 virtual machine that can be used for any purpose, especially sandboxing code. If require() is exposed by default, then your sandbox has landmines. If you want, you can expose stuff to the vm context like this:

var vm = require('vm')
var ret = vm.createScript('typeof require', 'sandbox.vm').runInNewContext({require:require})

Passing in require like this is really bad because it leaks variables. There has to be a better alternative. I agree with @isaacs it shouldn't be default, however perhaps an optional flag for the vm to have it's own instance of require. Unless theres a drop in replacement for require that would work, I've been looking into felixge/node-sandboxed-module and I believe it does the opposite of what we need.

@bnoordhuis
Copy link
Member

I admit I don't really understand what you mean by "it leaks variables" or what you are trying to accomplish but I think the short answer is "you can't do that", require() is too deeply tied into the node run-time for that. Contexts created with the vm module are not node contexts, they're just JS contexts.

@isaacs
Copy link
Contributor

isaacs commented Aug 24, 2015

If you want to pass in a specific function called require() into a vm, then you would have to write that function, and pass it in, like node does.

@bnoordhuis I presume that "leaks variables" means that it exposes internals to the untrusted code in the sandbox.

@reggi
Copy link
Contributor Author

reggi commented Aug 24, 2015

Yes by "leaking variables" I mean that when I pass require into a vm, that require is attached to the parent environment. So when that require runs within the vm it effects the module that is running the vm and not the environment within the vm.

var vm = require('vm')
var code = [
  'Array.marker = true',
  "require(\'./global.js\')", // (changes Array.marker to false)
  'console.log(Array.marker)' // => true [unexpected]
].join('\n')
vm.runInNewContext(code, {
  require: require,
  console: console
})
console.log(Array.marker) // => false [unexpected]

In my opinion this is unexpected behavior. 👎 😿 🌋 🚒

I really don't know how to write a require function without using vm, which will cause the same issue.

@reggi
Copy link
Contributor Author

reggi commented Aug 24, 2015

Perhaps this pertains to nodejs/node-v0.x-archive#9211?

mnahkies added a commit to mnahkies/openapi-code-generator that referenced this issue Apr 7, 2024
adds support for the validation keywords defined for strings in
https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#section-6.3

originally I tried to create `/RegExp literals/` but this didn't work
for the test harness with `joi` due to
nodejs/node#2503 (comment) and a
reliance on `pattern instanceof RegExp` check that `joi` makes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Issues that look for answers. vm Issues and PRs related to the vm subsystem.
Projects
None yet
Development

No branches or pull requests

4 participants