Skip to content

Commit dc7a032

Browse files
committed
nx-compile 2.0
1 parent bcc5168 commit dc7a032

File tree

4 files changed

+208
-140
lines changed

4 files changed

+208
-140
lines changed

README.md

Lines changed: 68 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# nx-compile
22

3-
This library is part of the [NX framework](http://nx-nxframework.rhcloud.com/).
4-
The purpose of this library is to allow the execution of strings as code in a secure, sandboxed environment.
3+
This library is part of the [NX framework](http://nx-framework.com/).
4+
The purpose of this library is to allow the execution of strings as code in the
5+
context of a sandbox object with optional security restrictions.
56

67
## Installation
78

@@ -27,93 +28,77 @@ const compiler = require('@risingstack/nx-compile')
2728

2829
## API
2930

30-
### compiler.compileCode(String)
31+
### compiler.compileCode(String, Object)
3132

32-
This method creates a function out of a string and returns it. The returned function executes the string as code in a sandbox. The string can be any valid javascript code.
33+
This method creates a function out of a string and returns it. The returned function executes the string as code in the passed sandbox object. The string can be any valid javascript code and it is
34+
always executed in strict mode.
3335

3436
```js
3537
const code = compiler.compileCode('const sum = prop1 + prop2')
3638
```
3739

38-
### compiler.compileExpression(String)
40+
### compiler.compileExpression(String, Object)
3941

40-
This method creates a function out of a string and returns it. The returned function executes the string as an expression in a sandbox and returns the result of this execution. The string can be any javascript code that may follow a return statement.
42+
This method creates a function out of a string and returns it. The returned function executes the string as an expression in the passed sandbox object and returns the result of this execution. The string can be any javascript code that may follow a return statement and it is always executed in
43+
strict mode.
4144

4245
```js
4346
const expression = compiler.compileExpression('prop1 || prop2')
4447
```
4548

46-
### using the returned function, expression(Object, [Array | true])
49+
### compiler.secure(String, ..., String)
4750

48-
Pass an object as the first argument, this will be the sandbox the function executes in. Optionally you can expose global variables by passing an Array of variable names as second argument. Passing true as the second argument exposes every global variable.
51+
This methods secures the sandbox and the compiled code. It prevents access to the global scope
52+
from inside the passed code. You can optionally pass global variable names as strings to expose
53+
them to the sandbox. Exposed global variables and the prototypes of literals (strings, numbers, etc.)
54+
are deep frozen with Object.freeze() to prevent security leaks. Deep frozen means that their whole prototype chain and all constructors found on that prototype chain are frozen. Calling secure more than once throws an error.
4955

5056
```js
51-
const result = expression({prop1: 'someValue', prop2: 'someOtherValue'}, ['LocalStorage'])
57+
compiler.secure('console', 'setTimeout')
5258
```
5359

60+
This method is experimental, please do not use it in a production environment yet!
61+
5462
## Example
5563

5664
```js
5765
const compiler = require('@risingstack/nx-compile')
66+
compiler.secure('console')
5867

59-
const code = compiler.compileCode('console.log(name + version)')
68+
const sandbox = {name: 'nx-compile', version: '1.0'}
69+
const code = compiler.compileCode('console.log(name + version)', sandbox)
6070

6171
// outputs 'nx-compile1.0' to console
62-
code({name: 'nx-compile', version: '1.0'}, ['console'])
63-
64-
// outputs 'undefined1.0' to console (name is undefined)
65-
code({version: '1.0'}, ['console'])
66-
67-
// throws a ReferenceError (console is undefined)
68-
code({name: 'nx-compile', version: '1.0'})
72+
code()
6973
```
7074

7175
## Features, limitations and edge cases
7276

73-
#### difference between global and sandbox variables
74-
75-
Javascript throws a ReferenceError if you try to read undeclared variables. The sandbox is more forgiving. It reads it as undefined instead.
76-
77-
```js
78-
const compiler = require('@risingstack/nx-compile')
79-
80-
const code = compiler.compileCode('console.log(nonExistentVar)')
81-
82-
// tries to retrieve 'nonExistentVar' from the (forgiving) sandbox
83-
// outputs 'undefined' to the console
84-
code({}, ['console'])
85-
86-
// tries to retrieve 'nonExistentVar' from the global object
87-
// throws a ReferenceError
88-
code({}, ['console', 'nonExistentVar'])
89-
```
90-
9177
#### lookup order
9278

93-
The compiled function tries to retrieve the variables first from the sandbox and then from the global object (if exposed by the second parameter).
79+
The compiled function tries to retrieve the variables first from the sandbox and then from the global object.
9480

9581
```js
9682
const compiler = require('@risingstack/nx-compile')
9783

9884
global.prop = 'globalValue' // in a browser global would be window
9985
const sandbox = {prop: 'sandboxValue'}
10086

101-
const code = compiler.compileCode('console.log(prop)')
87+
const code = compiler.compileCode('console.log(prop)', sandbox)
10288

10389
// outputs 'sandboxValue' to the console
104-
code(sandbox, ['console', 'prop'])
90+
code()
10591

106-
sandbox.prop = undefined
10792

10893
// the key is still present in the sandbox
10994
// outputs 'undefined' to the console
110-
code(sandbox, ['console', 'prop'])
111-
112-
delete sandbox.prop
95+
sandbox.prop = undefined
96+
code()
11397

11498
// the key is not present in the sandbox
11599
// outputs 'globalValue' to the console
116-
code(sandbox, ['console', 'prop'])
100+
delete sandbox.prop
101+
code()
117102
```
118103

119104
#### local variables can't be exposed
@@ -125,11 +110,11 @@ You can only expose variables declared on the global object.
125110
const compiler = require('@risingstack/nx-compile')
126111

127112
const localVariable = 'localValue'
128-
const code = compiler.compileCode('console.log(localVariable)')
113+
const code = compiler.compileCode('console.log(localVariable)', {})
129114

130115
// tries to retrieve 'localVariable' from the global object
131116
// throws a ReferenceError
132-
code({}, ['console', 'localVariable'])
117+
code()
133118
```
134119

135120
#### 'this' inside the sandboxed code
@@ -140,10 +125,11 @@ code({}, ['console', 'localVariable'])
140125
const compiler = require('@risingstack/nx-compile')
141126

142127
const message = 'local message'
143-
const code = compiler.compileCode('console.log(this.message)')
128+
const sandbox = {message: 'sandboxed message'}
129+
const code = compiler.compileCode('console.log(this.message)', sandbox)
144130

145131
// outputs 'sandboxed message' to the console
146-
code({message: 'sandboxed message'}, ['console'])
132+
code()
147133
```
148134

149135
#### functions defined inside the sandboxed code
@@ -154,13 +140,43 @@ Functions defined inside the sandboxed code are also sandboxed.
154140
const compiler = require('@risingstack/nx-compile')
155141

156142
const message = 'local message'
157-
const rawCode = '({}).__proto__.func = function() { return message }'
158-
const code = compiler.compileCode(rawCode)
159-
160-
code({message: 'sandboxed message'})
143+
const sandbox = {message: 'sandboxed message'}
144+
const code = compiler.compileCode('setTimeout(() => console.log(message))', sandbox)
161145

162146
// outputs 'sandboxed message' to the console
163-
console.log(({}).func())
147+
code()
148+
```
149+
150+
#### globals in secure mode
151+
152+
Unexposed global variable access is prevented in secure mode.
153+
154+
```js
155+
const compiler = require('@risingstack/nx-compile')
156+
compiler.secure('console')
157+
158+
const sandbox = {}
159+
const code = compiler.compileCode('console.log(setTimeout)', sandbox)
160+
161+
// console is exposed, setTimeout is not exposed
162+
// outputs 'undefined' to the console
163+
code()
164+
```
165+
166+
#### frozen objects in secure mode
167+
168+
Exposed globals and literal prototypes are frozen in secure mode.
169+
170+
```js
171+
const compiler = require('@risingstack/nx-compile')
172+
compiler.secure('console')
173+
174+
const sandbox = {}
175+
const rawCode = '({}).constructor.create = function(/* evil stuff */) {}'
176+
const code = compiler.compileCode(, sandbox)
177+
178+
// throws a TypeError, Object.create() can not be overwritten
179+
code()
164180
```
165181

166182
## Contributions

compiler.js

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,35 @@
11
'use strict'
22

3-
const sandboxProxies = new WeakMap()
4-
let currentAllowedGlobals
3+
let secured = false
4+
let exposedGlobals = []
55

66
module.exports = {
77
compileCode,
8-
compileExpression
8+
compileExpression,
9+
secure
910
}
1011

11-
function compileExpression (src) {
12-
if (typeof src !== 'string') {
13-
throw new TypeError('first argument must be a string')
14-
}
15-
return compileCode(`return ${src}`)
12+
function compileExpression (src, sandbox) {
13+
return compileCode(`return ${src}`, sandbox)
1614
}
1715

18-
function compileCode (src) {
16+
function compileCode (src, sandbox) {
1917
if (typeof src !== 'string') {
2018
throw new TypeError('first argument must be a string')
2119
}
20+
if (typeof sandbox !== 'object') {
21+
throw new TypeError('second argument must be an object')
22+
}
2223

23-
const code = new Function('sandbox', `with (sandbox) {${src}}`) // eslint-disable-line
24-
25-
return function (sandbox, allowedGlobals) {
26-
if (typeof sandbox !== 'object') {
27-
throw new TypeError('first argument must be an object')
28-
}
29-
if (allowedGlobals !== undefined && allowedGlobals !== true && !Array.isArray(allowedGlobals)) {
30-
throw new TypeError('second argument must be an array of strings or true or undefined')
31-
}
24+
if (secured) {
25+
sandbox = new Proxy(sandbox, {get, has})
26+
}
3227

33-
if (!sandboxProxies.has(sandbox)) {
34-
sandboxProxies.set(sandbox, new Proxy(sandbox, {has, get}))
35-
}
36-
currentAllowedGlobals = allowedGlobals
37-
const sandboxProxy = sandboxProxies.get(sandbox)
28+
// test for string manipulation
29+
new Function(`'use strict'; ${src}`) // eslint-disable-line
3830

39-
let result
40-
try {
41-
result = code.call(sandboxProxy, sandboxProxy)
42-
} finally {
43-
currentAllowedGlobals = undefined
44-
}
45-
return result
46-
}
31+
return new Function(`with (this) { return (() => { 'use strict'; ${src} }) }`) // eslint-disable-line
32+
.call(sandbox)
4733
}
4834

4935
function get (target, key, receiver) {
@@ -54,17 +40,41 @@ function get (target, key, receiver) {
5440
}
5541

5642
function has (target, key) {
57-
if (isAllowedGlobal(key)) {
43+
if (exposedGlobals.indexOf(key) !== -1) {
5844
return Reflect.has(target, key)
5945
}
6046
return true
6147
}
6248

63-
function isAllowedGlobal (key) {
64-
if (currentAllowedGlobals === true) {
65-
return true
49+
function secure () {
50+
if (secured) {
51+
throw new Error('the compiler is already secured')
6652
}
67-
if (Array.isArray(currentAllowedGlobals) && currentAllowedGlobals.indexOf(key) !== -1) {
68-
return true
53+
54+
exposedGlobals.push(...arguments)
55+
const globalObject = getGlobalObject()
56+
for (let exposed of exposedGlobals) {
57+
deepFreeze(globalObject[exposed])
6958
}
59+
60+
const literals = ['', 0, true, /a/, [], {}, () => {}]
61+
for (let literal of literals) {
62+
deepFreeze(Object.getPrototypeOf(literal))
63+
}
64+
secured = true
65+
}
66+
67+
function deepFreeze (obj) {
68+
if ((typeof obj === 'object' || typeof obj === 'function') && obj !== null && !Object.isFrozen(obj)) {
69+
Object.freeze(obj)
70+
Object.freeze(obj.constructor)
71+
deepFreeze(Object.getPrototypeOf(obj))
72+
}
73+
}
74+
75+
function getGlobalObject () {
76+
if (typeof global === 'object' && global.global === global) return global
77+
if (typeof self === 'object' && self.self === self) return self // eslint-disable-line
78+
if (typeof window === 'object' && window.window === window) return window
79+
throw new Error('global object could not be detected')
7080
}

0 commit comments

Comments
 (0)