@jsreport/jsreport-core 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/main/logger.js +3 -0
- package/lib/main/optionsLoad.js +10 -2
- package/lib/main/optionsSchema.js +6 -3
- package/lib/main/reporter.js +26 -4
- package/lib/worker/render/executeEngine.js +78 -20
- package/lib/worker/render/moduleHelper.js +4 -4
- package/lib/worker/render/profiler.js +7 -2
- package/lib/worker/reporter.js +6 -4
- package/lib/worker/sandbox/{safeSandbox.js → createSandbox.js} +90 -35
- package/lib/worker/sandbox/runInSandbox.js +37 -12
- package/package.json +5 -5
package/lib/main/logger.js
CHANGED
|
@@ -166,6 +166,9 @@ function configureLogger (logger, _transports) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
for (const { TransportClass, options } of transportsToAdd) {
|
|
169
|
+
if (options.silent) {
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
169
172
|
const transportInstance = new TransportClass(options)
|
|
170
173
|
|
|
171
174
|
const existingTransport = logger.transports.find((t) => t.name === transportInstance.name)
|
package/lib/main/optionsLoad.js
CHANGED
|
@@ -39,6 +39,7 @@ async function optionsLoad ({
|
|
|
39
39
|
loadConfigResult = await loadConfig(defaults, options, false)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const explicitOptions = loadConfigResult[0]
|
|
42
43
|
const appliedConfigFile = loadConfigResult[1]
|
|
43
44
|
|
|
44
45
|
options.loadConfig = shouldLoadExternalConfig
|
|
@@ -79,9 +80,16 @@ async function optionsLoad ({
|
|
|
79
80
|
options.store = options.store || { provider: 'memory' }
|
|
80
81
|
|
|
81
82
|
options.sandbox = options.sandbox || {}
|
|
82
|
-
|
|
83
|
+
|
|
84
|
+
// NOTE: handling back-compatible introduction of "trustUserCode" option, "allowLocalFilesAccess" is deprecated
|
|
85
|
+
if (explicitOptions.allowLocalFilesAccess === true && explicitOptions.trustUserCode == null) {
|
|
86
|
+
options.trustUserCode = true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (options.trustUserCode === true) {
|
|
83
90
|
options.sandbox.allowedModules = '*'
|
|
84
91
|
}
|
|
92
|
+
|
|
85
93
|
options.sandbox.nativeModules = options.sandbox.nativeModules || []
|
|
86
94
|
options.sandbox.modules = options.sandbox.modules || []
|
|
87
95
|
options.sandbox.allowedModules = options.sandbox.allowedModules || []
|
|
@@ -98,7 +106,7 @@ async function optionsLoad ({
|
|
|
98
106
|
fs.mkdirSync(options.tempCoreDirectory, { recursive: true })
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
return appliedConfigFile
|
|
109
|
+
return [explicitOptions, appliedConfigFile]
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
/**
|
|
@@ -59,6 +59,7 @@ module.exports.getRootSchemaOptions = () => ({
|
|
|
59
59
|
default: '2s'
|
|
60
60
|
},
|
|
61
61
|
enableRequestReportTimeout: { type: 'boolean', default: false, description: 'option that enables passing a custom report timeout per request using req.options.timeout. this enables that the caller of the report generation control the report timeout so enable it only when you trust the caller' },
|
|
62
|
+
trustUserCode: { type: 'boolean', default: false, description: 'option that control whether code sandboxing is enabled or not, code sandboxing has an impact on performance when rendering large reports. when true code sandboxing will be disabled meaning that users can potentially penetrate the local system if you allow code from external users to be part of your reports' },
|
|
62
63
|
allowLocalFilesAccess: { type: 'boolean', default: false },
|
|
63
64
|
encryption: {
|
|
64
65
|
type: 'object',
|
|
@@ -77,6 +78,7 @@ module.exports.getRootSchemaOptions = () => ({
|
|
|
77
78
|
},
|
|
78
79
|
sandbox: {
|
|
79
80
|
type: 'object',
|
|
81
|
+
default: {},
|
|
80
82
|
properties: {
|
|
81
83
|
allowedModules: {
|
|
82
84
|
anyOf: [{
|
|
@@ -89,9 +91,10 @@ module.exports.getRootSchemaOptions = () => ({
|
|
|
89
91
|
},
|
|
90
92
|
cache: {
|
|
91
93
|
type: 'object',
|
|
94
|
+
default: {},
|
|
92
95
|
properties: {
|
|
93
|
-
max: { type: 'number' },
|
|
94
|
-
enabled: { type: 'boolean' }
|
|
96
|
+
max: { type: 'number', default: 100 },
|
|
97
|
+
enabled: { type: 'boolean', default: true }
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
}
|
|
@@ -182,7 +185,7 @@ module.exports.getRootSchemaOptions = () => ({
|
|
|
182
185
|
'$jsreport-acceptsDuration': true,
|
|
183
186
|
default: '1m'
|
|
184
187
|
},
|
|
185
|
-
|
|
188
|
+
maxDiffSize: {
|
|
186
189
|
type: ['string', 'number'],
|
|
187
190
|
'$jsreport-acceptsSize': true,
|
|
188
191
|
default: '50mb'
|
package/lib/main/reporter.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
const path = require('path')
|
|
7
7
|
const { Readable } = require('stream')
|
|
8
|
-
const Reaper = require('
|
|
8
|
+
const Reaper = require('@jsreport/reap')
|
|
9
9
|
const optionsLoad = require('./optionsLoad')
|
|
10
10
|
const { createLogger, configureLogger, silentLogs } = require('./logger')
|
|
11
11
|
const checkEntityName = require('./validateEntityName')
|
|
@@ -90,8 +90,8 @@ class MainReporter extends Reporter {
|
|
|
90
90
|
return this
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
async extensionsLoad (
|
|
94
|
-
const appliedConfigFile = await optionsLoad({
|
|
93
|
+
async extensionsLoad (_opts = {}) {
|
|
94
|
+
const [explicitOptions, appliedConfigFile] = await optionsLoad({
|
|
95
95
|
defaults: this.defaults,
|
|
96
96
|
options: this.options,
|
|
97
97
|
validator: this.optionsValidator,
|
|
@@ -106,6 +106,8 @@ class MainReporter extends Reporter {
|
|
|
106
106
|
silentLogs(this.logger)
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
const { onConfigDetails, ...opts } = _opts
|
|
110
|
+
|
|
109
111
|
this.logger.info(`Initializing jsreport (version: ${this.version}, configuration file: ${appliedConfigFile || 'none'}, nodejs: ${process.versions.node})`)
|
|
110
112
|
|
|
111
113
|
await this.extensionsManager.load(opts)
|
|
@@ -130,6 +132,10 @@ class MainReporter extends Reporter {
|
|
|
130
132
|
throw new Error(`options contain values that does not match the defined full root schema. ${rootOptionsValidation.fullErrorMessage}`)
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
if (typeof onConfigDetails === 'function') {
|
|
136
|
+
onConfigDetails(explicitOptions)
|
|
137
|
+
}
|
|
138
|
+
|
|
133
139
|
return this
|
|
134
140
|
}
|
|
135
141
|
|
|
@@ -157,6 +163,7 @@ class MainReporter extends Reporter {
|
|
|
157
163
|
*/
|
|
158
164
|
async init () {
|
|
159
165
|
this.closing = this.closed = false
|
|
166
|
+
|
|
160
167
|
if (this._initialized || this._initializing) {
|
|
161
168
|
throw new Error('jsreport already initialized or just initializing. Make sure init is called only once')
|
|
162
169
|
}
|
|
@@ -175,7 +182,14 @@ class MainReporter extends Reporter {
|
|
|
175
182
|
|
|
176
183
|
try {
|
|
177
184
|
this._registerLogMainAction()
|
|
178
|
-
|
|
185
|
+
|
|
186
|
+
let explicitOptions
|
|
187
|
+
|
|
188
|
+
await this.extensionsLoad({
|
|
189
|
+
onConfigDetails: (_explicitOptions) => {
|
|
190
|
+
explicitOptions = _explicitOptions
|
|
191
|
+
}
|
|
192
|
+
})
|
|
179
193
|
|
|
180
194
|
this.documentStore = DocumentStore(Object.assign({}, this.options, { logger: this.logger }), this.entityTypeValidator, this.encryption)
|
|
181
195
|
documentStoreActions(this)
|
|
@@ -200,6 +214,14 @@ class MainReporter extends Reporter {
|
|
|
200
214
|
|
|
201
215
|
await this.extensionsManager.init()
|
|
202
216
|
|
|
217
|
+
if (this.options.trustUserCode) {
|
|
218
|
+
this.logger.info('Code sandboxing is disabled, users can potentially penetrate the local system if you allow code from external users to be part of your reports')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (explicitOptions.trustUserCode == null && explicitOptions.allowLocalFilesAccess != null) {
|
|
222
|
+
this.logger.warn('options.allowLocalFilesAccess is deprecated, use options.trustUserCode instead')
|
|
223
|
+
}
|
|
224
|
+
|
|
203
225
|
this.logger.info(`Using general timeout for rendering (reportTimeout: ${this.options.reportTimeout})`)
|
|
204
226
|
|
|
205
227
|
if (this.options.store.provider === 'memory') {
|
|
@@ -9,9 +9,10 @@ const LRU = require('lru-cache')
|
|
|
9
9
|
const { nanoid } = require('nanoid')
|
|
10
10
|
|
|
11
11
|
module.exports = (reporter) => {
|
|
12
|
-
const
|
|
12
|
+
const templatesCache = LRU(reporter.options.sandbox.cache)
|
|
13
|
+
let systemHelpersCache
|
|
13
14
|
|
|
14
|
-
reporter.templatingEngines = { cache }
|
|
15
|
+
reporter.templatingEngines = { cache: templatesCache }
|
|
15
16
|
|
|
16
17
|
const executionFnParsedParamsMap = new Map()
|
|
17
18
|
const executionAsyncResultsMap = new Map()
|
|
@@ -60,6 +61,33 @@ module.exports = (reporter) => {
|
|
|
60
61
|
evaluate: async (executionInfo, entityInfo) => {
|
|
61
62
|
return templatingEnginesEvaluate(false, executionInfo, entityInfo, req)
|
|
62
63
|
},
|
|
64
|
+
waitForAsyncHelper: async (maybeAsyncContent) => {
|
|
65
|
+
if (
|
|
66
|
+
context.__executionId == null ||
|
|
67
|
+
!executionAsyncResultsMap.has(context.__executionId) ||
|
|
68
|
+
typeof maybeAsyncContent !== 'string'
|
|
69
|
+
) {
|
|
70
|
+
return maybeAsyncContent
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const asyncResultMap = executionAsyncResultsMap.get(context.__executionId)
|
|
74
|
+
const asyncHelperResultRegExp = /{#asyncHelperResult ([^{}]+)}/
|
|
75
|
+
let content = maybeAsyncContent
|
|
76
|
+
let matchResult
|
|
77
|
+
|
|
78
|
+
do {
|
|
79
|
+
if (matchResult != null) {
|
|
80
|
+
const matchedPart = matchResult[0]
|
|
81
|
+
const asyncResultId = matchResult[1]
|
|
82
|
+
const result = await asyncResultMap.get(asyncResultId)
|
|
83
|
+
content = `${content.slice(0, matchResult.index)}${result}${content.slice(matchResult.index + matchedPart.length)}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
matchResult = content.match(asyncHelperResultRegExp)
|
|
87
|
+
} while (matchResult != null)
|
|
88
|
+
|
|
89
|
+
return content
|
|
90
|
+
},
|
|
63
91
|
waitForAsyncHelpers: async () => {
|
|
64
92
|
if (context.__executionId != null && executionAsyncResultsMap.has(context.__executionId)) {
|
|
65
93
|
const asyncResultMap = executionAsyncResultsMap.get(context.__executionId)
|
|
@@ -102,22 +130,49 @@ module.exports = (reporter) => {
|
|
|
102
130
|
entityPath = await reporter.folders.resolveEntityPath(entity, entitySet, req)
|
|
103
131
|
}
|
|
104
132
|
|
|
105
|
-
const
|
|
106
|
-
const
|
|
133
|
+
const normalizedHelpers = `${helpers || ''}`
|
|
134
|
+
const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${normalizedHelpers}`
|
|
107
135
|
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
136
|
+
const initFn = async (getTopLevelFunctions, compileScript) => {
|
|
137
|
+
if (systemHelpersCache != null) {
|
|
138
|
+
return systemHelpersCache
|
|
111
139
|
}
|
|
112
140
|
|
|
113
|
-
|
|
114
|
-
|
|
141
|
+
const registerResults = await reporter.registerHelpersListeners.fire()
|
|
142
|
+
const systemHelpers = []
|
|
143
|
+
|
|
144
|
+
for (const result of registerResults) {
|
|
145
|
+
if (result == null) {
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof result === 'string') {
|
|
150
|
+
systemHelpers.push(result)
|
|
151
|
+
}
|
|
115
152
|
}
|
|
116
|
-
}
|
|
117
153
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
const systemHelpersStr = systemHelpers.join('\n')
|
|
155
|
+
|
|
156
|
+
const functionNames = getTopLevelFunctions(systemHelpersStr)
|
|
157
|
+
|
|
158
|
+
const exposeSystemHelpersCode = `for (const fName of ${JSON.stringify(functionNames)}) { this[fName] = __topLevelFunctions[fName] }`
|
|
159
|
+
|
|
160
|
+
// we sync the __topLevelFunctions with system helpers and expose it immediately to the global context
|
|
161
|
+
const userCode = `(async () => { ${systemHelpersStr};
|
|
162
|
+
__topLevelFunctions = {...__topLevelFunctions, ${functionNames.map(h => `"${h}": ${h}`).join(',')}}; ${exposeSystemHelpersCode}
|
|
163
|
+
})()`
|
|
164
|
+
|
|
165
|
+
const filename = 'system-helpers.js'
|
|
166
|
+
const script = compileScript(userCode, filename)
|
|
167
|
+
|
|
168
|
+
systemHelpersCache = {
|
|
169
|
+
filename,
|
|
170
|
+
source: systemHelpersStr,
|
|
171
|
+
script
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return systemHelpersCache
|
|
175
|
+
}
|
|
121
176
|
|
|
122
177
|
const executionFn = async ({ require, console, topLevelFunctions, context }) => {
|
|
123
178
|
const asyncResultMap = new Map()
|
|
@@ -129,17 +184,16 @@ module.exports = (reporter) => {
|
|
|
129
184
|
|
|
130
185
|
const key = `template:${content}:${engine.name}`
|
|
131
186
|
|
|
132
|
-
if (!
|
|
187
|
+
if (!templatesCache.has(key)) {
|
|
133
188
|
try {
|
|
134
|
-
|
|
189
|
+
templatesCache.set(key, engine.compile(content, { require }))
|
|
135
190
|
} catch (e) {
|
|
136
191
|
e.property = 'content'
|
|
137
192
|
throw e
|
|
138
193
|
}
|
|
139
194
|
}
|
|
140
195
|
|
|
141
|
-
const compiledTemplate =
|
|
142
|
-
|
|
196
|
+
const compiledTemplate = templatesCache.get(key)
|
|
143
197
|
const wrappedTopLevelFunctions = {}
|
|
144
198
|
|
|
145
199
|
for (const h of Object.keys(topLevelFunctions)) {
|
|
@@ -147,7 +201,9 @@ module.exports = (reporter) => {
|
|
|
147
201
|
}
|
|
148
202
|
|
|
149
203
|
let contentResult = await engine.execute(compiledTemplate, wrappedTopLevelFunctions, data, { require })
|
|
204
|
+
|
|
150
205
|
const resolvedResultsMap = new Map()
|
|
206
|
+
|
|
151
207
|
while (asyncResultMap.size > 0) {
|
|
152
208
|
await Promise.all([...asyncResultMap.keys()].map(async (k) => {
|
|
153
209
|
resolvedResultsMap.set(k, `${await asyncResultMap.get(k)}`)
|
|
@@ -178,14 +234,16 @@ module.exports = (reporter) => {
|
|
|
178
234
|
return executionFn({ require, console, topLevelFunctions, context })
|
|
179
235
|
} else {
|
|
180
236
|
const awaiter = {}
|
|
237
|
+
|
|
181
238
|
awaiter.promise = new Promise((resolve) => {
|
|
182
239
|
awaiter.resolve = resolve
|
|
183
240
|
})
|
|
241
|
+
|
|
184
242
|
executionFnParsedParamsMap.get(req.context.id).set(executionFnParsedParamsKey, awaiter)
|
|
185
243
|
}
|
|
186
244
|
|
|
187
245
|
if (reporter.options.sandbox.cache && reporter.options.sandbox.cache.enabled === false) {
|
|
188
|
-
|
|
246
|
+
templatesCache.reset()
|
|
189
247
|
}
|
|
190
248
|
|
|
191
249
|
try {
|
|
@@ -193,10 +251,10 @@ module.exports = (reporter) => {
|
|
|
193
251
|
context: {
|
|
194
252
|
...(engine.createContext ? engine.createContext() : {})
|
|
195
253
|
},
|
|
196
|
-
userCode:
|
|
254
|
+
userCode: normalizedHelpers,
|
|
255
|
+
initFn,
|
|
197
256
|
executionFn,
|
|
198
257
|
currentPath: entityPath,
|
|
199
|
-
errorLineNumberOffset: systemHelpersStr.split('\n').length,
|
|
200
258
|
onRequire: (moduleName, { context }) => {
|
|
201
259
|
if (engine.onRequire) {
|
|
202
260
|
return engine.onRequire(moduleName, { context })
|
|
@@ -4,7 +4,7 @@ const path = require('path')
|
|
|
4
4
|
module.exports = (reporter) => {
|
|
5
5
|
let helpersScript
|
|
6
6
|
|
|
7
|
-
reporter.registerHelpersListeners.add('core-helpers', (
|
|
7
|
+
reporter.registerHelpersListeners.add('core-helpers', () => {
|
|
8
8
|
return helpersScript
|
|
9
9
|
})
|
|
10
10
|
|
|
@@ -12,11 +12,11 @@ module.exports = (reporter) => {
|
|
|
12
12
|
helpersScript = await fs.readFile(path.join(__dirname, '../../static/helpers.js'), 'utf8')
|
|
13
13
|
})
|
|
14
14
|
|
|
15
|
-
reporter.extendProxy((proxy, req, {
|
|
15
|
+
reporter.extendProxy((proxy, req, { sandboxRequire }) => {
|
|
16
16
|
proxy.module = async (module) => {
|
|
17
|
-
if (!reporter.options.
|
|
17
|
+
if (!reporter.options.trustUserCode && reporter.options.sandbox.allowedModules !== '*') {
|
|
18
18
|
if (reporter.options.sandbox.allowedModules.indexOf(module) === -1) {
|
|
19
|
-
throw reporter.createError(`require of module ${module} was rejected. Either set
|
|
19
|
+
throw reporter.createError(`require of module ${module} was rejected. Either set trustUserCode=true or sandbox.allowLocalModules='*' or sandbox.allowLocalModules=['${module}'] `, { status: 400 })
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -72,7 +72,7 @@ class Profiler {
|
|
|
72
72
|
let content = res.content
|
|
73
73
|
|
|
74
74
|
if (content != null) {
|
|
75
|
-
if (content.length > this.reporter.options.profiler.
|
|
75
|
+
if (content.length > this.reporter.options.profiler.maxDiffSize) {
|
|
76
76
|
content = {
|
|
77
77
|
tooLarge: true
|
|
78
78
|
}
|
|
@@ -97,7 +97,12 @@ class Profiler {
|
|
|
97
97
|
|
|
98
98
|
const stringifiedReq = JSON.stringify({ template: req.template, data: req.data }, null, 2)
|
|
99
99
|
|
|
100
|
-
m.req = {
|
|
100
|
+
m.req = { }
|
|
101
|
+
if (stringifiedReq.length * 4 > this.reporter.options.profiler.maxDiffSize) {
|
|
102
|
+
m.req.tooLarge = true
|
|
103
|
+
} else {
|
|
104
|
+
m.req.diff = createPatch('req', req.context.profiling.reqLastVal || '', stringifiedReq, 0)
|
|
105
|
+
}
|
|
101
106
|
|
|
102
107
|
req.context.profiling.resLastVal = (res.content == null || isbinaryfile(res.content) || content.tooLarge) ? null : res.content.toString()
|
|
103
108
|
req.context.profiling.resMetaLastVal = stringifiedResMeta
|
package/lib/worker/reporter.js
CHANGED
|
@@ -111,14 +111,14 @@ class WorkerReporter extends Reporter {
|
|
|
111
111
|
this._proxyRegistrationFns.push(registrationFn)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
createProxy ({ req, runInSandbox, context, getTopLevelFunctions,
|
|
114
|
+
createProxy ({ req, runInSandbox, context, getTopLevelFunctions, sandboxRequire }) {
|
|
115
115
|
const proxyInstance = {}
|
|
116
116
|
for (const fn of this._proxyRegistrationFns) {
|
|
117
117
|
fn(proxyInstance, req, {
|
|
118
118
|
runInSandbox,
|
|
119
119
|
context,
|
|
120
120
|
getTopLevelFunctions,
|
|
121
|
-
|
|
121
|
+
sandboxRequire
|
|
122
122
|
})
|
|
123
123
|
}
|
|
124
124
|
return proxyInstance
|
|
@@ -137,10 +137,11 @@ class WorkerReporter extends Reporter {
|
|
|
137
137
|
manager,
|
|
138
138
|
context,
|
|
139
139
|
userCode,
|
|
140
|
+
initFn,
|
|
140
141
|
executionFn,
|
|
142
|
+
currentPath,
|
|
141
143
|
onRequire,
|
|
142
144
|
propertiesConfig,
|
|
143
|
-
currentPath,
|
|
144
145
|
errorLineNumberOffset
|
|
145
146
|
}, req) {
|
|
146
147
|
// we flush before running code in sandbox because it can potentially
|
|
@@ -152,10 +153,11 @@ class WorkerReporter extends Reporter {
|
|
|
152
153
|
manager,
|
|
153
154
|
context,
|
|
154
155
|
userCode,
|
|
156
|
+
initFn,
|
|
155
157
|
executionFn,
|
|
158
|
+
currentPath,
|
|
156
159
|
onRequire,
|
|
157
160
|
propertiesConfig,
|
|
158
|
-
currentPath,
|
|
159
161
|
errorLineNumberOffset
|
|
160
162
|
}, req)
|
|
161
163
|
}
|
|
@@ -19,6 +19,7 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
19
19
|
propertiesConfig = {},
|
|
20
20
|
globalModules = [],
|
|
21
21
|
allowedModules = [],
|
|
22
|
+
safeExecution,
|
|
22
23
|
requireMap
|
|
23
24
|
} = options
|
|
24
25
|
|
|
@@ -65,7 +66,7 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
if (allowAllModules || allowedModules === '*') {
|
|
69
|
+
if (!safeExecution || allowAllModules || allowedModules === '*') {
|
|
69
70
|
return doRequire(moduleName, requirePaths, modulesCache)
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -111,14 +112,28 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
111
112
|
require: (m) => _require(m, { context: _sandbox })
|
|
112
113
|
})
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
let safeVM
|
|
116
|
+
let vmSandbox
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
delete vm.sandbox.global
|
|
118
|
+
if (safeExecution) {
|
|
119
|
+
safeVM = new VM()
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
// delete the vm.sandbox.global because it introduces json stringify issues
|
|
122
|
+
// and we don't need such global in context
|
|
123
|
+
delete safeVM.sandbox.global
|
|
124
|
+
|
|
125
|
+
for (const name in sandbox) {
|
|
126
|
+
safeVM.setGlobal(name, sandbox[name])
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
vmSandbox = safeVM.sandbox
|
|
130
|
+
} else {
|
|
131
|
+
vmSandbox = originalVM.createContext(undefined)
|
|
132
|
+
vmSandbox.Buffer = Buffer
|
|
133
|
+
|
|
134
|
+
for (const name in sandbox) {
|
|
135
|
+
vmSandbox[name] = sandbox[name]
|
|
136
|
+
}
|
|
122
137
|
}
|
|
123
138
|
|
|
124
139
|
// processing top level props because getter/setter descriptors
|
|
@@ -127,49 +142,46 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
127
142
|
const currentConfig = propsConfig[key]
|
|
128
143
|
|
|
129
144
|
if (currentConfig.root && currentConfig.root.sandboxReadOnly) {
|
|
130
|
-
readOnlyProp(
|
|
145
|
+
readOnlyProp(vmSandbox, key, [], customProxies, { onlyTopLevel: true })
|
|
131
146
|
}
|
|
132
147
|
})
|
|
133
148
|
|
|
134
149
|
const sourceFilesInfo = new Map()
|
|
135
150
|
|
|
136
151
|
return {
|
|
137
|
-
sandbox:
|
|
152
|
+
sandbox: vmSandbox,
|
|
138
153
|
console: _console,
|
|
139
154
|
sourceFilesInfo,
|
|
155
|
+
compileScript: (code, filename) => {
|
|
156
|
+
return doCompileScript(code, filename, safeExecution)
|
|
157
|
+
},
|
|
140
158
|
restore: () => {
|
|
141
|
-
return restoreProperties(
|
|
159
|
+
return restoreProperties(vmSandbox, originalValues, proxiesInVM, customProxies)
|
|
142
160
|
},
|
|
143
|
-
|
|
144
|
-
run: async (
|
|
145
|
-
|
|
161
|
+
sandboxRequire: (modulePath) => _require(modulePath, { context: _sandbox, allowAllModules: true }),
|
|
162
|
+
run: async (codeOrScript, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) => {
|
|
163
|
+
let run
|
|
146
164
|
|
|
147
165
|
if (filename != null && source != null) {
|
|
148
166
|
sourceFilesInfo.set(filename, { filename, source, entity, entitySet, errorLineNumberOffset })
|
|
149
167
|
}
|
|
150
168
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
importModuleDynamically: () => {
|
|
164
|
-
// We can't throw an error object here because since vm.Script doesn't store a context, we can't properly contextify that error object.
|
|
165
|
-
// eslint-disable-next-line no-throw-literal
|
|
166
|
-
throw 'Dynamic imports are not allowed.'
|
|
167
|
-
}
|
|
168
|
-
})
|
|
169
|
+
const script = typeof codeOrScript !== 'string' ? codeOrScript : doCompileScript(codeOrScript, filename, safeExecution)
|
|
170
|
+
|
|
171
|
+
if (safeExecution) {
|
|
172
|
+
run = async () => {
|
|
173
|
+
return safeVM.run(script)
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
run = async () => {
|
|
177
|
+
return script.runInContext(vmSandbox, {
|
|
178
|
+
displayErrors: true
|
|
179
|
+
})
|
|
180
|
+
}
|
|
169
181
|
}
|
|
170
182
|
|
|
171
183
|
try {
|
|
172
|
-
const result = await
|
|
184
|
+
const result = await run()
|
|
173
185
|
return result
|
|
174
186
|
} catch (e) {
|
|
175
187
|
decorateErrorMessage(e, sourceFilesInfo)
|
|
@@ -180,10 +192,53 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
180
192
|
}
|
|
181
193
|
}
|
|
182
194
|
|
|
195
|
+
function doCompileScript (code, filename, safeExecution) {
|
|
196
|
+
let script
|
|
197
|
+
|
|
198
|
+
if (safeExecution) {
|
|
199
|
+
script = new VMScript(code, filename)
|
|
200
|
+
|
|
201
|
+
// NOTE: if we need to upgrade vm2 we will need to check the source of this function
|
|
202
|
+
// in vm2 repo and see if we need to change this,
|
|
203
|
+
// we needed to override this method because we want "displayErrors" to be true in order
|
|
204
|
+
// to show nice error when the compile of a script fails
|
|
205
|
+
script._compile = function (prefix, suffix) {
|
|
206
|
+
return new originalVM.Script(prefix + this.getCompiledCode() + suffix, {
|
|
207
|
+
__proto__: null,
|
|
208
|
+
filename: this.filename,
|
|
209
|
+
displayErrors: true,
|
|
210
|
+
lineOffset: this.lineOffset,
|
|
211
|
+
columnOffset: this.columnOffset,
|
|
212
|
+
// THIS FN WAS TAKEN FROM vm2 source, nothing special here
|
|
213
|
+
importModuleDynamically: () => {
|
|
214
|
+
// We can't throw an error object here because since vm.Script doesn't store a context, we can't properly contextify that error object.
|
|
215
|
+
// eslint-disable-next-line no-throw-literal
|
|
216
|
+
throw 'Dynamic imports are not allowed.'
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// do the compilation
|
|
222
|
+
script._compileVM()
|
|
223
|
+
} else {
|
|
224
|
+
script = new originalVM.Script(code, {
|
|
225
|
+
filename,
|
|
226
|
+
displayErrors: true,
|
|
227
|
+
importModuleDynamically: () => {
|
|
228
|
+
// We can't throw an error object here because since vm.Script doesn't store a context, we can't properly contextify that error object.
|
|
229
|
+
// eslint-disable-next-line no-throw-literal
|
|
230
|
+
throw 'Dynamic imports are not allowed.'
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return script
|
|
236
|
+
}
|
|
237
|
+
|
|
183
238
|
function doRequire (moduleName, requirePaths = [], modulesCache) {
|
|
184
239
|
const searchedPaths = []
|
|
185
240
|
|
|
186
|
-
function
|
|
241
|
+
function optimizedRequire (require, modulePath) {
|
|
187
242
|
// save the current module cache, we will use this to restore the cache to the
|
|
188
243
|
// original values after the require finish
|
|
189
244
|
const originalModuleCache = Object.assign(Object.create(null), require.cache)
|
|
@@ -238,13 +293,13 @@ function doRequire (moduleName, requirePaths = [], modulesCache) {
|
|
|
238
293
|
}
|
|
239
294
|
}
|
|
240
295
|
|
|
241
|
-
let result =
|
|
296
|
+
let result = optimizedRequire(require, moduleName)
|
|
242
297
|
|
|
243
298
|
if (!result) {
|
|
244
299
|
let pathsSearched = 0
|
|
245
300
|
|
|
246
301
|
while (!result && pathsSearched < requirePaths.length) {
|
|
247
|
-
result =
|
|
302
|
+
result = optimizedRequire(require, path.join(requirePaths[pathsSearched], moduleName))
|
|
248
303
|
pathsSearched++
|
|
249
304
|
}
|
|
250
305
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
const LRU = require('lru-cache')
|
|
2
2
|
const stackTrace = require('stack-trace')
|
|
3
3
|
const { customAlphabet } = require('nanoid')
|
|
4
|
-
const
|
|
4
|
+
const createSandbox = require('./createSandbox')
|
|
5
5
|
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
|
|
6
6
|
|
|
7
7
|
module.exports = (reporter) => {
|
|
8
|
-
|
|
8
|
+
const functionsCache = LRU(reporter.options.sandbox.cache)
|
|
9
|
+
|
|
10
|
+
return async ({
|
|
9
11
|
manager = {},
|
|
10
12
|
context,
|
|
11
13
|
userCode,
|
|
14
|
+
initFn,
|
|
12
15
|
executionFn,
|
|
13
16
|
currentPath,
|
|
14
17
|
onRequire,
|
|
@@ -19,7 +22,8 @@ module.exports = (reporter) => {
|
|
|
19
22
|
|
|
20
23
|
// we use dynamic name because of the potential nested vm2 execution in the jsreportProxy.assets.require
|
|
21
24
|
// it may turn out it is a bad approach in assets so we gonna delete it here
|
|
22
|
-
const executionFnName = nanoid()
|
|
25
|
+
const executionFnName = `${nanoid()}_executionFn`
|
|
26
|
+
|
|
23
27
|
context[executionFnName] = executionFn
|
|
24
28
|
context.__appDirectory = reporter.options.appDirectory
|
|
25
29
|
context.__rootDirectory = reporter.options.rootDirectory
|
|
@@ -28,13 +32,14 @@ module.exports = (reporter) => {
|
|
|
28
32
|
context.__topLevelFunctions = {}
|
|
29
33
|
context.__handleError = (err) => handleError(reporter, err)
|
|
30
34
|
|
|
31
|
-
const { sourceFilesInfo, run, restore, sandbox,
|
|
35
|
+
const { sourceFilesInfo, run, compileScript, restore, sandbox, sandboxRequire } = createSandbox(context, {
|
|
32
36
|
onLog: (log) => {
|
|
33
37
|
reporter.logger[log.level](log.message, { ...req, timestamp: log.timestamp })
|
|
34
38
|
},
|
|
35
39
|
formatError: (error, moduleName) => {
|
|
36
|
-
error.message += ` To be able to require custom modules you need to add to configuration { "
|
|
40
|
+
error.message += ` To be able to require custom modules you need to add to configuration { "trustUserCode": true } or enable just specific module using { sandbox: { allowedModules": ["${moduleName}"] }`
|
|
37
41
|
},
|
|
42
|
+
safeExecution: reporter.options.trustUserCode === false,
|
|
38
43
|
modulesCache: reporter.requestModulesCache.get(req.context.rootId),
|
|
39
44
|
globalModules: reporter.options.sandbox.nativeModules || [],
|
|
40
45
|
allowedModules: reporter.options.sandbox.allowedModules,
|
|
@@ -61,7 +66,17 @@ module.exports = (reporter) => {
|
|
|
61
66
|
}
|
|
62
67
|
})
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
const _getTopLevelFunctions = (code) => {
|
|
70
|
+
return getTopLevelFunctions(functionsCache, code)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
jsreportProxy = reporter.createProxy({
|
|
74
|
+
req,
|
|
75
|
+
runInSandbox: run,
|
|
76
|
+
context: sandbox,
|
|
77
|
+
getTopLevelFunctions: _getTopLevelFunctions,
|
|
78
|
+
sandboxRequire
|
|
79
|
+
})
|
|
65
80
|
|
|
66
81
|
jsreportProxy.currentPath = async () => {
|
|
67
82
|
// we get the current path by throwing an error, which give us a stack trace
|
|
@@ -114,7 +129,18 @@ module.exports = (reporter) => {
|
|
|
114
129
|
// be passed in options
|
|
115
130
|
manager.restore = restore
|
|
116
131
|
|
|
117
|
-
|
|
132
|
+
if (typeof initFn === 'function') {
|
|
133
|
+
const initScriptInfo = await initFn(_getTopLevelFunctions, compileScript)
|
|
134
|
+
|
|
135
|
+
if (initScriptInfo) {
|
|
136
|
+
await run(initScriptInfo.script, {
|
|
137
|
+
filename: initScriptInfo.filename || 'sandbox-init.js',
|
|
138
|
+
source: initScriptInfo.source
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const functionNames = getTopLevelFunctions(functionsCache, userCode)
|
|
118
144
|
const functionsCode = `return {${functionNames.map(h => `"${h}": ${h}`).join(',')}}`
|
|
119
145
|
const executionCode = `;(async () => { ${userCode} \n\n;${functionsCode} })()
|
|
120
146
|
.then((topLevelFunctions) => {
|
|
@@ -180,12 +206,11 @@ function handleError (reporter, errValue) {
|
|
|
180
206
|
})
|
|
181
207
|
}
|
|
182
208
|
|
|
183
|
-
|
|
184
|
-
function getTopLevelFunctions (code) {
|
|
209
|
+
function getTopLevelFunctions (cache, code) {
|
|
185
210
|
const key = `functions:${code}`
|
|
186
211
|
|
|
187
|
-
if (
|
|
188
|
-
return
|
|
212
|
+
if (cache.has(key)) {
|
|
213
|
+
return cache.get(key)
|
|
189
214
|
}
|
|
190
215
|
|
|
191
216
|
// lazy load to speed up boot
|
|
@@ -223,6 +248,6 @@ function getTopLevelFunctions (code) {
|
|
|
223
248
|
return []
|
|
224
249
|
}
|
|
225
250
|
|
|
226
|
-
|
|
251
|
+
cache.set(key, names)
|
|
227
252
|
return names
|
|
228
253
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsreport/jsreport-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "javascript based business reporting",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"report",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"@babel/code-frame": "7.12.13",
|
|
33
33
|
"@babel/parser": "7.14.4",
|
|
34
34
|
"@babel/traverse": "7.12.9",
|
|
35
|
-
"@jsreport/advanced-workers": "1.2.
|
|
35
|
+
"@jsreport/advanced-workers": "1.2.2",
|
|
36
36
|
"@jsreport/mingo": "2.4.1",
|
|
37
37
|
"ajv": "6.12.6",
|
|
38
|
-
"app-root-path": "
|
|
38
|
+
"app-root-path": "3.0.0",
|
|
39
39
|
"bytes": "3.1.2",
|
|
40
40
|
"camelcase": "5.0.0",
|
|
41
41
|
"debug": "4.3.2",
|
|
@@ -56,14 +56,14 @@
|
|
|
56
56
|
"nanoid": "3.2.0",
|
|
57
57
|
"nconf": "0.12.0",
|
|
58
58
|
"node.extend.without.arrays": "1.1.6",
|
|
59
|
-
"
|
|
59
|
+
"@jsreport/reap": "0.1.0",
|
|
60
60
|
"semver": "7.3.5",
|
|
61
61
|
"serializator": "1.0.2",
|
|
62
62
|
"stack-trace": "0.0.10",
|
|
63
63
|
"triple-beam": "1.3.0",
|
|
64
64
|
"unset-value": "1.0.0",
|
|
65
65
|
"uuid": "8.3.2",
|
|
66
|
-
"vm2": "3.9.
|
|
66
|
+
"vm2": "3.9.9",
|
|
67
67
|
"winston": "3.3.3",
|
|
68
68
|
"winston-transport": "4.4.0"
|
|
69
69
|
},
|