Skip to content

Commit

Permalink
Set uid and gid to the evaluator
Browse files Browse the repository at this point in the history
This protects from a vulnerability related to #9.

Ensuring the code runs in a process with no rights,
ensure that any vulnerability cannot get undue access,
thanks to OS-level protections.

It is a strong safety guarantee.
  • Loading branch information
espadrine committed Oct 3, 2021
1 parent 4ea3516 commit ce985eb
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 9 deletions.
6 changes: 6 additions & 0 deletions child.js
Expand Up @@ -2,6 +2,12 @@ var vm = require('vm');
var fs = require('fs');
try {
var input = JSON.parse(fs.readFileSync(0, 'utf-8'));
if (input.uid !== undefined && process.setuid !== undefined) {
process.setuid(input.uid);
}
if (input.gid !== undefined && process.setgid !== undefined) {
process.setgid(input.gid);
}
var output = vm.runInNewContext(input.code, input.sandbox);
fs.writeFileSync(1, JSON.stringify({ output }));
} catch(error) {
Expand Down
23 changes: 20 additions & 3 deletions localeval.js
Expand Up @@ -27,9 +27,20 @@ if (node_js) {
child = cp.fork(childPath);
};

var evaluator = function(code, sandbox, timeout, cb) {
var evaluator = function(code, sandbox, options = {}, cb) {
if (typeof options === 'number') {
var timeout = options;
options = {timeout: timeout};
} else {
var timeout = options.timeout;
}
// Optional parameters: sandbox, timeout, cb.
var childInput = JSON.stringify({ code, sandbox });
var childInput = JSON.stringify({
code,
sandbox,
uid: options.uid,
gid: options.gid,
});
var spawnOptions = {
timeout,
env: {},
Expand Down Expand Up @@ -245,7 +256,13 @@ if (node_js) {
worker = new Worker('worker.js');
};

var localeval = function(source, sandbox, timeout, cb) {
var localeval = function(source, sandbox, options, cb) {
if (typeof options === 'number') {
var timeout = options;
options = {timeout: timeout};
} else {
var timeout = options.timeout;
}
// Optional parameters: sandbox, timeout, cb.
if (timeout != null) {
// We have a timeout. Run in web worker.
Expand Down
19 changes: 14 additions & 5 deletions readme.md
Expand Up @@ -4,20 +4,29 @@ Evaluate a string of JS code without access to the global object.

In Node.js, always use that instead of `eval()`. Always.

In the browser, do not expect it to be able to safely execute untrusted code
yet.

API:

localeval(code :: String, sandbox :: Object) :: Object.

localeval(code :: String, sandbox :: Object,
timeout :: Number, cb :: Function)
localeval(code :: String, sandbox :: Object,
options :: Object, cb :: Function)

- `code`: string of JS code.
- `sandbox`: object whose values will be in the global object in the sandbox.
- `options`: object containing the following optional fields:
- `timeout`: number of milliseconds that the child process has to run the
code, beyond which it will be killed.
- `uid`: user id under which the child process must be set, if any.
- `gid`: group id under which the child process must be set, if any.

The `code` is a string of JS code. The `sandbox` contains objects which are
going to be accessible in the JS code.
It returns the last evaluated piece of JS code in `code`, if no timeout is
given. Otherwise, after at most `timeout` milliseconds, the callback gives that
result as a parameter: `function(error, result) {…}`.

Node example:
Node.js example:

```javascript
var localeval = require('localeval');
Expand Down
7 changes: 6 additions & 1 deletion test/spec.js
Expand Up @@ -49,5 +49,10 @@ describe('synchronous localeval', function() {
localeval(`this.constructor.constructor('process.exit(0)')()`),
/Failed localeval execution/);
});
it('uid and gid', function() {
assert.throws(() =>
localeval(`this.constructor.constructor("process.kill(process.ppid)")()`,
{}, {uid: 'games'}),
/EPERM/);
});
});

0 comments on commit ce985eb

Please sign in to comment.