Skip to content

Commit fe411a2

Browse files
Merge pull request #118 from SylvainCorlay/worker-eval
Safe conditional formatting
2 parents 2fb8195 + b3c2955 commit fe411a2

File tree

11 files changed

+106
-128
lines changed

11 files changed

+106
-128
lines changed

README.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ $ jupyter labextension install ipysheet
4343
If you have notebook 5.2 or below, you also need to execute:
4444
```
4545
$ jupyter nbextension enable --py --sys-prefix ipysheet
46-
$ jupyter nbextension enable --py --sys-prefix ipysheet.renderer_nbext
4746
```
4847

4948
For a development installation (requires npm),
@@ -54,19 +53,7 @@ $ cd ipysheet
5453
$ pip install -e .
5554
$ jupyter nbextension install --py --symlink --sys-prefix ipysheet
5655
$ jupyter nbextension enable --py --sys-prefix ipysheet
57-
$ jupyter nbextension enable --py --sys-prefix ipysheet.renderer_nbext
5856
$ jupyter labextension link js
5957
```
6058

6159
For Jupyter lab development, you may want to start Jupyter lab with `jupyter lab --watch` so it instantly picks up changes.
62-
63-
# Security
64-
65-
*If you are a regular Jupyter notebook or lab user you can ignore this section, it is only relevant is shared multiusers environment, like with Jupyter hub.*
66-
67-
ipysheet contains a part (the Renderer widget) that will allow arbitrary Javascript injection from a user into a webpage that contains the notebook. In situation where notebooks are shared, this can lead to security issues. If you want to disable this, run for the Jupyter notebook and Jupyter lab respectively:
68-
69-
```
70-
$ jupyter nbextension disable --py --sys-prefix ipysheet.renderer_nbext
71-
$ jupyter labextension disable ipysheet:renderer # for jupyter lab
72-
```

docs/source/index.ipynb

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
"If you have notebook 5.2 or below, you also need to execute:\n",
2828
"```\n",
2929
"$ jupyter nbextension enable --py --sys-prefix ipysheet\n",
30-
"$ jupyter nbextension enable --py --sys-prefix ipysheet.renderer_nbext\n",
3130
"```"
3231
]
3332
},
@@ -46,13 +45,13 @@
4645
},
4746
{
4847
"cell_type": "code",
49-
"execution_count": 1,
48+
"execution_count": 20,
5049
"metadata": {},
5150
"outputs": [
5251
{
5352
"data": {
5453
"application/vnd.jupyter.widget-view+json": {
55-
"model_id": "f4ff470ae36f47fbbd41fec1c0f33c72",
54+
"model_id": "416721e4ee6e49acb6e16247cd941edf",
5655
"version_major": 2,
5756
"version_minor": 0
5857
},
@@ -79,13 +78,13 @@
7978
},
8079
{
8180
"cell_type": "code",
82-
"execution_count": 2,
81+
"execution_count": 21,
8382
"metadata": {},
8483
"outputs": [
8584
{
8685
"data": {
8786
"application/vnd.jupyter.widget-view+json": {
88-
"model_id": "ed50a15183c44808b15ff47fe550e6ad",
87+
"model_id": "ff3f93d6b9f4478c88f715adfe2e62e4",
8988
"version_major": 2,
9089
"version_minor": 0
9190
},
@@ -129,13 +128,13 @@
129128
},
130129
{
131130
"cell_type": "code",
132-
"execution_count": 3,
131+
"execution_count": 22,
133132
"metadata": {},
134133
"outputs": [
135134
{
136135
"data": {
137136
"application/vnd.jupyter.widget-view+json": {
138-
"model_id": "4377673eadff475cbc2ab6ebec1221d5",
137+
"model_id": "219ad954a15f4420962413c752383eef",
139138
"version_major": 2,
140139
"version_minor": 0
141140
},
@@ -179,13 +178,13 @@
179178
},
180179
{
181180
"cell_type": "code",
182-
"execution_count": 4,
181+
"execution_count": 23,
183182
"metadata": {},
184183
"outputs": [
185184
{
186185
"data": {
187186
"application/vnd.jupyter.widget-view+json": {
188-
"model_id": "ec061edbce744dae883ab84c70c99a39",
187+
"model_id": "dd536f9d0e074109aff4c567d9bfe1eb",
189188
"version_major": 2,
190189
"version_minor": 0
191190
},
@@ -216,13 +215,13 @@
216215
},
217216
{
218217
"cell_type": "code",
219-
"execution_count": 5,
218+
"execution_count": 24,
220219
"metadata": {},
221220
"outputs": [
222221
{
223222
"data": {
224223
"application/vnd.jupyter.widget-view+json": {
225-
"model_id": "84ba2d613f244d6598f18e96e71dc47a",
224+
"model_id": "253fc660f6d44a0984dbe77d491d73a1",
226225
"version_major": 2,
227226
"version_minor": 0
228227
},
@@ -257,36 +256,33 @@
257256
"metadata": {},
258257
"source": [
259258
"# Renderers\n",
260-
"ipysheet is build on Handsontable, which allows [custom renderers](https://docs.handsontable.com/demo-custom-renderers.html), which we also support. Note that this means ipysheet allows arbitrary JavaScript injection (TODO: make this part optional)"
259+
"ipysheet is build on Handsontable, which allows [custom renderers](https://docs.handsontable.com/demo-custom-renderers.html), which we also support."
261260
]
262261
},
263262
{
264263
"cell_type": "code",
265-
"execution_count": 6,
264+
"execution_count": 25,
266265
"metadata": {},
267266
"outputs": [],
268267
"source": [
269-
"jscode_renderer_negative = \"\"\"\n",
270-
"function (instance, td, row, col, prop, value, cellProperties) {\n",
271-
" Handsontable.renderers.TextRenderer.apply(this, arguments);\n",
272-
" if (value < 0)\n",
273-
" td.style.backgroundColor = 'red'\n",
274-
" else\n",
275-
" td.style.backgroundColor = 'green'\n",
268+
"jscode_renderer_negative = \"\"\"function (value) {\n",
269+
" return {\n",
270+
" backgroundColor: value < 0 ? 'red' : 'green'\n",
271+
" };\n",
276272
"}\n",
277273
"\"\"\"\n",
278274
"ipysheet.renderer(code=jscode_renderer_negative, name='negative');"
279275
]
280276
},
281277
{
282278
"cell_type": "code",
283-
"execution_count": 7,
279+
"execution_count": 26,
284280
"metadata": {},
285281
"outputs": [
286282
{
287283
"data": {
288284
"application/vnd.jupyter.widget-view+json": {
289-
"model_id": "3518aa67aad24dbda94d71ad73e4f1be",
285+
"model_id": "43e5068313f9468e93c0c16d8cd95de4",
290286
"version_major": 2,
291287
"version_minor": 0
292288
},
@@ -315,28 +311,26 @@
315311
},
316312
{
317313
"cell_type": "code",
318-
"execution_count": 8,
314+
"execution_count": 27,
319315
"metadata": {},
320316
"outputs": [],
321317
"source": [
322-
"def renderer_negative(instance, td, row, col, prop, value, cellProperties):\n",
323-
" Handsontable.renderers.TextRenderer.apply(this, arguments);\n",
324-
" if value < 0:\n",
325-
" td.style.backgroundColor = 'orange'\n",
326-
" else:\n",
327-
" td.style.backgroundColor = ''\n",
318+
"def renderer_negative(value):\n",
319+
" return {\n",
320+
" 'backgroundColor': 'orange' if value < 0 else ''\n",
321+
" }\n",
328322
"ipysheet.renderer(code=renderer_negative, name='negative_transpiled');"
329323
]
330324
},
331325
{
332326
"cell_type": "code",
333-
"execution_count": 9,
327+
"execution_count": 28,
334328
"metadata": {},
335329
"outputs": [
336330
{
337331
"data": {
338332
"application/vnd.jupyter.widget-view+json": {
339-
"model_id": "7e358c1b47034cd7aad1710a9e3c841b",
333+
"model_id": "707ebcff579849d28c0dea4669b0a6d8",
340334
"version_major": 2,
341335
"version_minor": 0
342336
},
@@ -383,7 +377,7 @@
383377
"name": "python",
384378
"nbconvert_exporter": "python",
385379
"pygments_lexer": "ipython3",
386-
"version": "3.6.4"
380+
"version": "3.7.3"
387381
},
388382
"widgets": {
389383
"application/vnd.jupyter.widget-state+json": {

ipysheet.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"load_extensions": {
3-
"ipysheet/extension": true,
4-
"ipysheet/extension-renderer": true
3+
"ipysheet/extension": true
54
}
6-
}
5+
}

ipysheet/renderer_nbext.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

ipysheet/sheet.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ def __getitem__(self, item):
121121

122122
class Renderer(widgets.Widget):
123123
_model_name = Unicode('RendererModel').tag(sync=True)
124-
_view_module = Unicode('ipysheet/renderer').tag(sync=True)
125-
_model_module = Unicode('ipysheet/renderer').tag(sync=True)
124+
_view_module = Unicode('ipysheet').tag(sync=True)
125+
_model_module = Unicode('ipysheet').tag(sync=True)
126126
_view_module_version = Unicode(semver_range_frontend).tag(sync=True)
127127
_model_module_version = Unicode(semver_range_frontend).tag(sync=True)
128128
name = Unicode('custom').tag(sync=True)

js/src/extension-renderer.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

js/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,5 @@
99

1010
// Export widget models and views, and the npm package version number.
1111
export * from './sheet';
12+
export * from './renderer';
1213
export { version } from './version';
13-
import * as Handsontable from 'handsontable';
14-
export { Handsontable };

js/src/renderer.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import * as widgets from '@jupyter-widgets/base';
2+
import * as Handsontable from 'handsontable';
23
import {extend} from 'lodash';
34
import {version, semver_range} from './version';
4-
// @ts-ignore
5-
import {Handsontable} from 'ipysheet';
6-
5+
import {safeEval} from './worker_eval';
76

87
let RendererModel = widgets.WidgetModel.extend({
98
defaults: function() {
@@ -17,9 +16,15 @@ let RendererModel = widgets.WidgetModel.extend({
1716
},
1817
initialize: function() {
1918
RendererModel.__super__.initialize.apply(this, arguments);
20-
// we add Handsontable manually as extra argument to put it in the scope
21-
this.fn = new Function('Handsontable', 'return (' + this.get('code') + ')');
22-
(Handsontable.renderers as any).registerRenderer(this.get('name'), this.fn(Handsontable));
19+
// We add Handsontable manually as extra argument to put it in the scope
20+
var that = this;
21+
this.fn = function (instance, td, row, col, prop, value, cellProperties) {
22+
Handsontable.renderers.TextRenderer.apply(this, arguments);
23+
safeEval(`(${that.get('code')})(${value})`).then(function(style) {
24+
(Object as any).assign(td.style, style);
25+
});
26+
};
27+
(Handsontable.renderers as any).registerRenderer(this.get('name'), this.fn);
2328
}
2429
});
2530

js/src/worker_eval.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
export function safeEval(untrustedCode) {
2+
return new Promise(function (resolve, reject) {
3+
var blobURL = URL.createObjectURL(new Blob([
4+
"(",
5+
function () {
6+
var _postMessage = postMessage;
7+
var _addEventListener = addEventListener;
8+
9+
(function (obj) {
10+
"use strict";
11+
12+
var current = obj;
13+
var keepProperties = [
14+
// required
15+
'Object', 'Function', 'Infinity', 'NaN', 'undefined', 'caches', 'TEMPORARY', 'PERSISTENT',
16+
// optional, but trivial to get back
17+
'Array', 'Boolean', 'Number', 'String', 'Symbol',
18+
// optional
19+
'Map', 'Math', 'Set',
20+
];
21+
22+
do {
23+
Object.getOwnPropertyNames(current).forEach(function (name) {
24+
if (keepProperties.indexOf(name) === -1) {
25+
delete current[name];
26+
}
27+
});
28+
current = Object.getPrototypeOf(current);
29+
}
30+
while (current !== Object.prototype);
31+
})(this);
32+
33+
_addEventListener("message", function (e) {
34+
var f = new Function("", "return (" + e.data + "\n);");
35+
_postMessage(f(), undefined);
36+
});
37+
}.toString(),
38+
")()"
39+
], {
40+
type: "application/javascript"
41+
}));
42+
43+
var worker = new Worker(blobURL);
44+
45+
URL.revokeObjectURL(blobURL);
46+
47+
worker.onmessage = function (evt) {
48+
worker.terminate();
49+
resolve(evt.data);
50+
};
51+
52+
worker.onerror = function (evt) {
53+
reject(new Error(evt.message));
54+
};
55+
56+
worker.postMessage(untrustedCode);
57+
58+
setTimeout(function () {
59+
worker.terminate();
60+
reject(new Error('The worker timed out.'));
61+
}, 1000);
62+
});
63+
}

0 commit comments

Comments
 (0)