Skip to content

Commit 4eb00aa

Browse files
committed
feat(version 4): remove security for more performance
1 parent 6ccf218 commit 4eb00aa

File tree

4 files changed

+100
-64
lines changed

4 files changed

+100
-64
lines changed

README.md

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# nx-compile
22

3-
This library is part of the [NX framework](http://nx-framework.com/).
3+
This library is part of the [NX framework](http://nx-framework.com).
4+
45
The purpose of this library is to allow the execution of strings as code in the
5-
context of a sandbox object.
6+
context of an object. It combines ES6 Proxies with the JavaScript `with` keyword to achieve this.
67

78
## Installation
89

@@ -15,7 +16,7 @@ $ npm install @risingstack/nx-compile
1516
- Node: 6 and above
1617
- Chrome: 49 and above (after browserified)
1718
- Firefox: 38 and above (after browserified)
18-
- Safari: Technical Preview, 10 and above (after browserified)
19+
- Safari: 10 and above (after browserified)
1920
- Edge: 12 and above (after browserified)
2021
- Opera: 36 and above (after browserified)
2122
- IE is not supported
@@ -28,39 +29,59 @@ const compiler = require('@risingstack/nx-compile')
2829

2930
## API
3031

31-
### compiler.compileCode(String, Object)
32+
### compiler.compileCode(String)
3233

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.
34+
This method creates a function out of a string and returns it. The returned function takes
35+
an object as argument and executes the string as code in the context of the passed object.
36+
The string can be any valid javascript code.
3537

3638
```js
37-
const code = compiler.compileCode('const sum = prop1 + prop2')
39+
const code = compiler.compileCode('return prop1 + prop2')
40+
const sum = code({prop1: 1, prop2: 2}) // sum is 3
3841
```
3942

40-
### compiler.compileExpression(String, Object)
43+
### compiler.compileExpression(String)
4144

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.
45+
This method creates a function out of a string and returns it. The returned function takes
46+
an object as argument and executes the string as an expression in the context of the passed object.
47+
It returns the result of the evaluated expression. The string can be any javascript expression
48+
that may come after a return statement.
4449

4550
```js
4651
const expression = compiler.compileExpression('prop1 || prop2')
52+
const result = expression({prop2: 'Hello'}) // result is 'Hello'
53+
```
54+
55+
Expressions return undefined instead of throwing a TypeError on invalid property access.
56+
This allows lazy initialization of your data.
57+
58+
```js
59+
const expression = compiler.compileExpression('item.name')
60+
const context = {}
61+
62+
let result = expression(context) // result is undefined, no error is thrown
63+
64+
context.item = {name: 'item name'}
65+
result = expression(context) // result is 'item name'
4766
```
4867

4968
## Example
5069

5170
```js
5271
const compiler = require('@risingstack/nx-compile')
5372

54-
const sandbox = {name: 'nx-compile', version: '3.0.0'}
55-
const expression = compiler.compileExpression('name + version', sandbox)
73+
const context = {name: 'nx-compile', version: '4.0.0'}
74+
const expression = compiler.compileExpression('name + version)', sandbox)
5675

57-
// outputs 'nx-compile3.0.0' to console
58-
console.log(expression())
76+
// outputs 'nx-compile4.0.0' to console
77+
console.log(expression(context))
5978
```
6079

6180
## Contributions
6281

63-
This library has the very specific purpose of supporting the [NX framework](https://github.com/RisingStack/nx-framework). Features should only be added, if they are used by the framework. Otherwise please fork.
82+
This library has the very specific purpose of supporting the
83+
[NX framework](https://github.com/RisingStack/nx-framework).
84+
Features should only be added, if they are used by the framework. Otherwise please fork.
6485

6586
Bug fixes, tests and doc updates are always welcome.
6687
Tests and linter (standardJS) must pass.

compiler.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,34 @@
22

33
module.exports = {
44
compileCode,
5-
compileExpression,
6-
sandbox
5+
compileExpression
76
}
87

8+
let globalObj
9+
if (typeof window !== 'undefined') globalObj = window // eslint-disable-line
10+
else if (typeof global !== 'undefined') globalObj = global // eslint-disable-line
11+
else if (typeof self !== 'undefined') globalObj = self // eslint-disable-line
12+
globalObj.$nxCompileToSandbox = toSandbox
13+
14+
const proxies = new WeakMap()
915
const expressionCache = new Map()
1016
const codeCache = new Map()
17+
const handlers = {has}
1118

1219
function compileExpression (src) {
1320
if (typeof src !== 'string') {
1421
throw new TypeError('first argument must be a string')
1522
}
1623
let expression = expressionCache.get(src)
1724
if (!expression) {
18-
expression = new Function('sandbox',
19-
`try { with (sandbox) { return ${src} } } catch (err) {
25+
expression = new Function('sandbox', // eslint-disable-line
26+
`sandbox = $nxCompileToSandbox(sandbox)
27+
try { with (sandbox) { return ${src} } } catch (err) {
2028
if (!(err instanceof ReferenceError || err instanceof TypeError)) throw err
2129
}`)
2230
expressionCache.set(src, expression)
2331
}
24-
return expression // eslint-disable-line
32+
return expression
2533
}
2634

2735
function compileCode (src) {
@@ -30,17 +38,24 @@ function compileCode (src) {
3038
}
3139
let code = codeCache.get(src)
3240
if (!code) {
33-
code = new Function('sandbox', `with (sandbox) { ${src} }`)
41+
code = new Function('sandbox', // eslint-disable-line
42+
`sandbox = $nxCompileToSandbox(sandbox)
43+
with (sandbox) { ${src} }`)
3444
codeCache.set(src, code)
3545
}
36-
return code // eslint-disable-line
46+
return code
3747
}
3848

39-
function sandbox (obj) {
49+
function toSandbox (obj) {
4050
if (typeof obj !== 'object') {
4151
throw new TypeError('first argument must be an object')
4252
}
43-
return new Proxy(obj, {has})
53+
let sandbox = proxies.get(obj)
54+
if (!sandbox) {
55+
sandbox = new Proxy(obj, handlers)
56+
proxies.set(obj, sandbox)
57+
}
58+
return sandbox
4459
}
4560

4661
function has () {

compiler.test.js

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,62 @@
33
const expect = require('chai').expect
44
const compiler = require('./compiler')
55

6-
global.prop1 = 3
7-
global.prop2 = 4
8-
const localProp = 1
6+
const localProp = 'localProp'
7+
global.globalProp = 'globalProp'
98

109
describe('nx-compile', () => {
1110
describe('compileCode()', () => {
12-
it('should throw TypeError on invalid source argument', () => {
13-
expect(() => compiler.compileCode({}, {})).to.throw(TypeError)
14-
expect(() => compiler.compileCode(undefined, {})).to.throw(TypeError)
15-
expect(() => compiler.compileCode(12, {})).to.throw(TypeError)
16-
})
17-
18-
it('should throw TypeError on invalid sandbox argument', () => {
19-
expect(() => compiler.compileExpression('prop1 + prop2', 12)).to.throw(TypeError)
20-
expect(() => compiler.compileExpression('prop1 + prop2', undefined)).to.throw(TypeError)
21-
expect(() => compiler.compileExpression('prop1 + prop2', '')).to.throw(TypeError)
11+
it('should throw a TypeError on non string source argument', () => {
12+
expect(() => compiler.compileCode({})).to.throw(TypeError)
13+
expect(() => compiler.compileCode(undefined)).to.throw(TypeError)
14+
expect(() => compiler.compileCode(12)).to.throw(TypeError)
2215
})
2316
})
2417

2518
describe('compileExpression()', () => {
26-
it('should throw TypeError on invalid source argument', () => {
27-
expect(() => compiler.compileCode({}, {})).to.throw(TypeError)
28-
expect(() => compiler.compileCode(undefined, {})).to.throw(TypeError)
29-
expect(() => compiler.compileCode(12, {})).to.throw(TypeError)
30-
})
31-
32-
it('should throw TypeError on invalid sandbox argument', () => {
33-
expect(() => compiler.compileExpression('prop1 + prop2', 12)).to.throw(TypeError)
34-
expect(() => compiler.compileExpression('prop1 + prop2', undefined)).to.throw(TypeError)
35-
expect(() => compiler.compileExpression('prop1 + prop2', '')).to.throw(TypeError)
19+
it('should throw a TypeError on non string source argument', () => {
20+
expect(() => compiler.compileCode({})).to.throw(TypeError)
21+
expect(() => compiler.compileCode(undefined)).to.throw(TypeError)
22+
expect(() => compiler.compileCode(12)).to.throw(TypeError)
3623
})
3724
})
3825

3926
describe('returned function (compiled code or expression)', () => {
27+
it('should throw a TypeError on non object sandbox argument', () => {
28+
const expression = compiler.compileExpression('prop1 + prop2')
29+
expect(() => expression()).to.throw(TypeError)
30+
expect(() => expression('string')).to.throw(TypeError)
31+
expect(() => expression(12)).to.throw(TypeError)
32+
})
33+
4034
it('should execute in the context of the sandbox', () => {
41-
const expression = compiler.compileExpression('prop1 + prop2', {prop1: 1, prop2: 2})
42-
expect(expression()).to.equal(3)
35+
const expression = compiler.compileExpression('prop1 + prop2')
36+
expect(expression({prop1: 1, prop2: 2})).to.equal(3)
4337
})
4438

4539
it('should not expose local variables', () => {
46-
const expression = compiler.compileExpression('localProp', {})
47-
expect(expression()).to.equal(undefined)
40+
const expression = compiler.compileExpression('localProp')
41+
expect(expression({})).to.equal(undefined)
4842
})
4943

50-
it('should favour sandbox variables over global ones', () => {
51-
const expression = compiler.compileExpression('prop1 + prop2', {prop1: 1, prop2: 2})
52-
expect(expression()).to.equal(3)
44+
it('should not expose global variables', () => {
45+
const expression = compiler.compileExpression('globalProp')
46+
expect(expression({})).to.equal(undefined)
5347
})
48+
})
5449

55-
it('should set "this" to the sandbox instead of the global object', () => {
56-
const expression = compiler.compileExpression('this.prop1 + this.prop2', {prop1: 1, prop2: 2})
57-
expect(expression()).to.equal(3)
50+
describe('returned function expression', () => {
51+
it('should return undefined instead of throwing on invalid property access', () => {
52+
const expression = compiler.compileExpression('inner.prop1')
53+
expect(() => expression({})).to.not.throw(TypeError)
54+
expect(expression({})).to.equal(undefined)
5855
})
56+
})
5957

60-
it('should set "this" to be undefined inside functions defined in the passed code', () => {
61-
const code = compiler.compileCode('(function () { return this })()', {})
62-
expect(code()).to.equal(undefined)
58+
describe('returned code expression', () => {
59+
it('should throw on invalid property access', () => {
60+
const code = compiler.compileCode('inner.prop1')
61+
expect(() => code({})).to.throw(TypeError)
6362
})
6463
})
6564
})

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@risingstack/nx-compile",
3-
"version": "3.0.0",
4-
"description": "Execution of strings as code in a sandboxed environment with optional security.",
3+
"version": "4.0.0",
4+
"description": "Execution of string as code in the context of an object, similarly to Object.eval().",
55
"main": "compiler.js",
66
"scripts": {
77
"test": "mocha compiler.test.js",
@@ -23,9 +23,10 @@
2323
"keywords": [
2424
"nx",
2525
"compile",
26-
"sandbox",
26+
"context",
2727
"eval",
28-
"execution"
28+
"execution",
29+
"code"
2930
],
3031
"devDependencies": {
3132
"chai": "3.5.0",

0 commit comments

Comments
 (0)