Skip to content

Commit 2555a51

Browse files
committed
3.0.0
1 parent dc7a032 commit 2555a51

File tree

4 files changed

+23
-246
lines changed

4 files changed

+23
-246
lines changed

README.md

Lines changed: 5 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This library is part of the [NX framework](http://nx-framework.com/).
44
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.
5+
context of a sandbox object.
66

77
## Installation
88

@@ -46,137 +46,16 @@ strict mode.
4646
const expression = compiler.compileExpression('prop1 || prop2')
4747
```
4848

49-
### compiler.secure(String, ..., String)
50-
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.
55-
56-
```js
57-
compiler.secure('console', 'setTimeout')
58-
```
59-
60-
This method is experimental, please do not use it in a production environment yet!
61-
6249
## Example
6350

6451
```js
6552
const compiler = require('@risingstack/nx-compile')
66-
compiler.secure('console')
67-
68-
const sandbox = {name: 'nx-compile', version: '1.0'}
69-
const code = compiler.compileCode('console.log(name + version)', sandbox)
70-
71-
// outputs 'nx-compile1.0' to console
72-
code()
73-
```
74-
75-
## Features, limitations and edge cases
76-
77-
#### lookup order
78-
79-
The compiled function tries to retrieve the variables first from the sandbox and then from the global object.
80-
81-
```js
82-
const compiler = require('@risingstack/nx-compile')
83-
84-
global.prop = 'globalValue' // in a browser global would be window
85-
const sandbox = {prop: 'sandboxValue'}
86-
87-
const code = compiler.compileCode('console.log(prop)', sandbox)
88-
89-
// outputs 'sandboxValue' to the console
90-
code()
91-
92-
93-
// the key is still present in the sandbox
94-
// outputs 'undefined' to the console
95-
sandbox.prop = undefined
96-
code()
97-
98-
// the key is not present in the sandbox
99-
// outputs 'globalValue' to the console
100-
delete sandbox.prop
101-
code()
102-
```
103-
104-
#### local variables can't be exposed
105-
106-
You can only expose variables declared on the global object.
107-
108-
```js
109-
// this code is assumed to run in a module, so declared variables are not global
110-
const compiler = require('@risingstack/nx-compile')
111-
112-
const localVariable = 'localValue'
113-
const code = compiler.compileCode('console.log(localVariable)', {})
114-
115-
// tries to retrieve 'localVariable' from the global object
116-
// throws a ReferenceError
117-
code()
118-
```
119-
120-
#### 'this' inside the sandboxed code
121-
122-
`this` points to the sandbox inside the sandboxed code.
123-
124-
```js
125-
const compiler = require('@risingstack/nx-compile')
126-
127-
const message = 'local message'
128-
const sandbox = {message: 'sandboxed message'}
129-
const code = compiler.compileCode('console.log(this.message)', sandbox)
130-
131-
// outputs 'sandboxed message' to the console
132-
code()
133-
```
134-
135-
#### functions defined inside the sandboxed code
136-
137-
Functions defined inside the sandboxed code are also sandboxed.
138-
139-
```js
140-
const compiler = require('@risingstack/nx-compile')
141-
142-
const message = 'local message'
143-
const sandbox = {message: 'sandboxed message'}
144-
const code = compiler.compileCode('setTimeout(() => console.log(message))', sandbox)
145-
146-
// outputs 'sandboxed message' to the console
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')
17353

174-
const sandbox = {}
175-
const rawCode = '({}).constructor.create = function(/* evil stuff */) {}'
176-
const code = compiler.compileCode(, sandbox)
54+
const sandbox = {name: 'nx-compile', version: '3.0.0'}
55+
const expression = compiler.compileExpression('name + version)', sandbox)
17756

178-
// throws a TypeError, Object.create() can not be overwritten
179-
code()
57+
// outputs 'nx-compile3.0.0' to console
58+
console.log(expression())
18059
```
18160

18261
## Contributions

compiler.js

Lines changed: 16 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
'use strict'
22

3-
let secured = false
4-
let exposedGlobals = []
5-
63
module.exports = {
74
compileCode,
8-
compileExpression,
9-
secure
5+
compileExpression
106
}
117

128
function compileExpression (src, sandbox) {
13-
return compileCode(`return ${src}`, sandbox)
9+
if (typeof src !== 'string') {
10+
throw new TypeError('first argument must be a string')
11+
}
12+
if (typeof sandbox !== 'object') {
13+
throw new TypeError('second argument must be an object')
14+
}
15+
16+
sandbox = new Proxy(sandbox, {get, has})
17+
const expression = `
18+
try { with (sandbox) { return ${src} } } catch (err) {
19+
if (!(err instanceof ReferenceError || err instanceof TypeError)) throw err
20+
}`
21+
return new Function('sandbox', expression).bind(sandbox, sandbox) // eslint-disable-line
1422
}
1523

1624
function compileCode (src, sandbox) {
@@ -21,15 +29,8 @@ function compileCode (src, sandbox) {
2129
throw new TypeError('second argument must be an object')
2230
}
2331

24-
if (secured) {
25-
sandbox = new Proxy(sandbox, {get, has})
26-
}
27-
28-
// test for string manipulation
29-
new Function(`'use strict'; ${src}`) // eslint-disable-line
30-
31-
return new Function(`with (this) { return (() => { 'use strict'; ${src} }) }`) // eslint-disable-line
32-
.call(sandbox)
32+
sandbox = new Proxy(sandbox, {get, has})
33+
return new Function('sandbox', `with (sandbox) { ${src} }`).bind(sandbox, sandbox) // eslint-disable-line
3334
}
3435

3536
function get (target, key, receiver) {
@@ -40,41 +41,5 @@ function get (target, key, receiver) {
4041
}
4142

4243
function has (target, key) {
43-
if (exposedGlobals.indexOf(key) !== -1) {
44-
return Reflect.has(target, key)
45-
}
4644
return true
4745
}
48-
49-
function secure () {
50-
if (secured) {
51-
throw new Error('the compiler is already secured')
52-
}
53-
54-
exposedGlobals.push(...arguments)
55-
const globalObject = getGlobalObject()
56-
for (let exposed of exposedGlobals) {
57-
deepFreeze(globalObject[exposed])
58-
}
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')
80-
}

compiler.test.js

Lines changed: 1 addition & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ const compiler = require('./compiler')
55

66
global.prop1 = 3
77
global.prop2 = 4
8-
global.propObj = { nestedProp: 'nestedProp' }
9-
global.isSecure = true
108
const localProp = 1
119

1210
describe('nx-compile', () => {
@@ -46,12 +44,7 @@ describe('nx-compile', () => {
4644

4745
it('should not expose local variables', () => {
4846
const expression = compiler.compileExpression('localProp', {})
49-
expect(expression).to.throw(ReferenceError)
50-
})
51-
52-
it('should expose global variables', () => {
53-
const expression = compiler.compileExpression('prop1 + prop2', {})
54-
expect(expression()).to.equal(7)
47+
expect(expression()).to.equal(undefined)
5548
})
5649

5750
it('should favour sandbox variables over global ones', () => {
@@ -69,64 +62,4 @@ describe('nx-compile', () => {
6962
expect(code()).to.equal(undefined)
7063
})
7164
})
72-
73-
describe('secure()', () => {
74-
before(() => compiler.secure('prop1', 'propObj', 'localProp', 'setTimeout'))
75-
beforeEach(() => global.isSecure = true)
76-
77-
it('should not expose unallowed globals to the sandbox', () => {
78-
const expression = compiler.compileExpression('prop2', {})
79-
expect(expression()).to.equal(undefined)
80-
})
81-
82-
it('should expose specified globals to the sandbox', () => {
83-
const expression = compiler.compileExpression('prop1', {})
84-
expect(expression()).to.equal(3)
85-
})
86-
87-
it('should not expose globals inside functions defined in the passed code', () => {
88-
const code = compiler.compileCode('(function () { isSecure = false })()', {})
89-
code()
90-
expect(global.isSecure).to.be.true
91-
})
92-
93-
it('should not expose globals inside async functions defined in the passed code', () => {
94-
const code = compiler.compileCode('setTimeout(() => isSecure = false)', {})
95-
code()
96-
expect(global.isSecure).to.be.true
97-
})
98-
99-
it('should protect against string manipulation', () => {
100-
expect(() => compiler.compileCode('isSecure=false})};function this(){}//', {})).to.throw(SyntaxError)
101-
expect(() => compiler.compileCode('})} isSecure = false; {({', {})).to.throw(SyntaxError)
102-
expect(global.isSecure).to.be.true
103-
})
104-
105-
it('should deep freeze the prototype of literals', () => {
106-
const literals = ['', 0, true, /o/, {}, [], () => {}]
107-
for (let literal of literals) {
108-
expect(Object.isFrozen(Object.getPrototypeOf(literal))).to.be.true
109-
}
110-
})
111-
112-
it('should deep freeze exposed global objects', () => {
113-
expect(Object.isFrozen(global.propObj)).to.be.true
114-
})
115-
116-
it('should generally protect against global object mutation', () => {
117-
const code1 = compiler.compileCode('("").__proto__.replace = function() {}', {})
118-
const code2 = compiler.compileCode('({}).__proto__.toJSON = function() {}', {})
119-
const code3 = compiler.compileCode('({}).constructor.create = function() {}', {})
120-
const code4 = compiler.compileCode('({}).constructor.prototype.hasOwnProperty = function() {}', {})
121-
122-
expect(code1).to.throw(TypeError)
123-
expect(code2).to.throw(TypeError)
124-
expect(code3).to.throw(TypeError)
125-
expect(code4).to.throw(TypeError)
126-
})
127-
128-
it('should throw on further secure() calls', () => {
129-
expect(() => compiler.secure('prop3')).to.throw(Error)
130-
})
131-
})
13265
})

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@risingstack/nx-compile",
3-
"version": "2.0.0",
3+
"version": "3.0.0",
44
"description": "Execution of strings as code in a sandboxed environment with optional security.",
55
"main": "compiler.js",
66
"scripts": {

0 commit comments

Comments
 (0)