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

Add handle recursive expressions #25

Closed
wants to merge 10 commits into from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased][unreleased]

- Prevent arbitrary js code execution
- Handle recursive expressions

## [0.0.1][] - 2022-05-13

Expand Down
24 changes: 22 additions & 2 deletions lib/sheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

const metavm = require('metavm');

const LIMIT_STEPS = 100;

const wrap = (target) =>
new Proxy(target, {
get: (target, prop) => {
Expand All @@ -16,10 +18,25 @@ const math = wrap(Math);

const getValue = (target, prop) => {
if (prop === 'Math') return math;
const { expressions, data } = target;
const { expressions, data, callChain } = target;
const propAmountSteps = Number(callChain.get(prop) ?? 0);
if (propAmountSteps > LIMIT_STEPS) {
throw new Error('Recursive expression error');
}
if (!expressions.has(prop)) return data.get(prop);
const expression = expressions.get(prop);
return expression();
let value = null;
try {
callChain.set(prop, propAmountSteps + 1);
value = expression();
if (value) callChain.set(prop, 0);
} catch (err) {
if (err.message === 'Maximum call stack size exceeded') {
throw new Error('Recursive expression error');
}
throw err;
}
return value;
};

const getCell = (target, prop) => {
Expand All @@ -31,9 +48,11 @@ const getCell = (target, prop) => {
const setCell = (target, prop, value) => {
target.expressions.delete(prop);
target.data.delete(prop);
target.callChain.set(prop, 0);

if (typeof value === 'string' && value[0] === '=') {
const src = '() => ' + value.substring(1);

const options = { context: target.context };
const script = metavm.createScript(prop, src, options);
target.expressions.set(prop, script.exports);
Expand All @@ -50,6 +69,7 @@ class Sheet {
this.values = new Proxy(this, { get: getValue });
this.context = metavm.createContext(this.values);
this.cells = new Proxy(this, { get: getCell, set: setCell });
this.callChain = new Map();
}
}

Expand Down
41 changes: 41 additions & 0 deletions test/unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,47 @@ metatests.test('Expression chain', async (test) => {
test.end();
});

metatests.test('Recursive expressions', async (test) => {
const sheet = new Sheet();
sheet.cells['A1'] = 100;
sheet.cells['B1'] = 2;
sheet.cells['C1'] = '= A1 *E1';
sheet.cells['D1'] = '=C1+8';
sheet.cells['E1'] = '=D1/2';
Comment on lines +35 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sheet.cells['C1'] = '= A1 *E1';
sheet.cells['D1'] = '=C1+8';
sheet.cells['E1'] = '=D1/2';
sheet.cells['C1'] = '= A1 * E1';
sheet.cells['D1'] = '= C1 + 8';
sheet.cells['E1'] = '= D1 / 2';


try {
test.strictSame(sheet.values['D1'], 208);
} catch (error) {
test.strictSame(error.constructor.name === 'Error', true);
test.strictSame(error.message, 'Recursive expression error');
}

try {
test.strictSame(sheet.values['E1'], 104);
} catch (error) {
test.strictSame(error.constructor.name === 'Error', true);
test.strictSame(error.message, 'Recursive expression error');
}
test.end();
});

metatests.test(
'Exit from recursive expression use Math.random',
async (test) => {
const sheet = new Sheet();
sheet.cells['A1'] = 100;
sheet.cells['B1'] = 2;
sheet.cells['C1'] = '=Math.round(Math.random()) ? A1 * E1 : 5';
Copy link

@blackakula blackakula Jul 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Math.random is not stable for use in unit tests. Of course, it is a low probability, that you will get 100 "one"s results in a row, but still, one of 2^100 test runs may fail with message:

Exit from recursive expression use Math.random ........ 0/1
  not ok fail
    --- wanted                                                                                     
    +++ found                                                                                      
    -[null]                                                                                        
    +{                                                                                             
    +  "message": "Recursive expression error"                                                     
    +  "name": "Error"                                                                             
    +  "stack": "Error: Recursive expression error                                                 

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, ok, thanks
I used this test to show 'exit from recursive expression' (only math.random came to mind)
when somebody will say how assignment in expression is work to I fix previous test

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use F1 as a counter, increment it each C1 cell calculation triggered and exit recursion after certain limit

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use F1 as a counter, increment it each C1 cell calculation triggered and exit recursion after certain limit

How we can change cell's value? @tshemsedinov
I written about it above

sheet.cells['D1'] = '=C1+8';
sheet.cells['E1'] = '=D1/2';

const D1 = sheet.values['D1'];
test.strictSame(!(D1 & 0) || D1 === 13, true);

test.end();
},
);

metatests.test('JavaScript Math', async (test) => {
const sheet = new Sheet();
sheet.cells['A1'] = 100;
Expand Down