@jsreport/jsreport-core
Advanced tools
Comparing version 3.12.0 to 4.0.0
@@ -164,10 +164,2 @@ const { getDefaultTempDirectory, getDefaultLoadConfig } = require('./defaults') | ||
}, | ||
migrateXlsxTemplatesToAssets: { | ||
type: 'boolean', | ||
default: true | ||
}, | ||
migrateResourcesToAssets: { | ||
type: 'boolean', | ||
default: true | ||
}, | ||
profiler: { | ||
@@ -174,0 +166,0 @@ type: 'object', |
@@ -9,2 +9,3 @@ /*! | ||
const Reaper = require('@jsreport/reap') | ||
const pkg = require('../../package.json') | ||
const optionsLoad = require('./optionsLoad') | ||
@@ -32,4 +33,2 @@ const { createLogger, configureLogger, silentLogs } = require('./logger') | ||
const Profiler = require('./profiler') | ||
const migrateXlsxTemplatesToAssets = require('./migration/xlsxTemplatesToAssets') | ||
const migrateResourcesToAssets = require('./migration/resourcesToAssets') | ||
const semver = require('semver') | ||
@@ -42,4 +41,4 @@ let reportCounter = 0 | ||
if (!semver.satisfies(process.versions.node, '>=16.11.0')) { | ||
throw this.createError('jsreport needs at least node 16.11.0 to run.') | ||
if (!semver.satisfies(process.versions.node, pkg.engines.node)) { | ||
throw this.createError(`jsreport needs at least node ${pkg.engines.node} to run.`) | ||
} | ||
@@ -178,10 +177,2 @@ | ||
if (this.compilation) { | ||
this.compilation.resource('vm2-events.js', require.resolve('vm2/lib/events.js')) | ||
this.compilation.resource('vm2-resolver-compat.js', require.resolve('vm2/lib/resolver-compat.js')) | ||
this.compilation.resource('vm2-resolver.js', require.resolve('vm2/lib/resolver.js')) | ||
this.compilation.resource('vm2-setup-node-sandbox.js', require.resolve('vm2/lib/setup-node-sandbox.js')) | ||
this.compilation.resource('vm2-setup-sandbox.js', require.resolve('vm2/lib/setup-sandbox.js')) | ||
} | ||
try { | ||
@@ -244,4 +235,8 @@ this._registerLogMainAction() | ||
await this.blobStorage.init() | ||
await this.settings.init(this.documentStore, this.authorization) | ||
await this.settings.init(this.documentStore, { | ||
authentication: this.authentication, | ||
authorization: this.authorization | ||
}) | ||
const extensionsForWorkers = this.extensionsManager.extensions.filter(e => e.worker) | ||
@@ -275,5 +270,2 @@ | ||
this.initializeListeners.insert(0, 'core-resources-migration', () => migrateResourcesToAssets(this)) | ||
this.initializeListeners.insert(0, 'core-xlsxTemplates-migration', () => migrateXlsxTemplatesToAssets(this)) | ||
await this.initializeListeners.fire() | ||
@@ -280,0 +272,0 @@ |
@@ -50,6 +50,6 @@ /*! | ||
Settings.prototype.init = async function (documentStore, authorization) { | ||
Settings.prototype.init = async function (documentStore, { authentication, authorization }) { | ||
this.documentStore = documentStore | ||
if (authorization != null) { | ||
if (authentication != null && authorization != null) { | ||
const col = documentStore.collection('settings') | ||
@@ -59,3 +59,3 @@ | ||
// we only care about modification listeners | ||
col.beforeInsertListeners.add('settings', (doc, req) => { | ||
col.beforeInsertListeners.add('settings', async (doc, req) => { | ||
if (req && req.context && req.context.skipAuthorization) { | ||
@@ -65,3 +65,5 @@ return | ||
if (req && req.context && req.context.user && !req.context.user.isAdmin) { | ||
const isAdmin = await authentication.isUserAdmin(req?.context?.user, req) | ||
if (req && req.context && req.context.user && !isAdmin) { | ||
throw authorization.createAuthorizationError(col.name) | ||
@@ -71,3 +73,3 @@ } | ||
col.beforeUpdateListeners.add('settings', (q, u, options, req) => { | ||
col.beforeUpdateListeners.add('settings', async (q, u, options, req) => { | ||
if (req && req.context && req.context.skipAuthorization) { | ||
@@ -77,3 +79,5 @@ return | ||
if (req && req.context && req.context.user && !req.context.user.isAdmin) { | ||
const isAdmin = await authentication.isUserAdmin(req?.context?.user, req) | ||
if (req && req.context && req.context.user && !isAdmin) { | ||
throw authorization.createAuthorizationError(col.name) | ||
@@ -83,3 +87,3 @@ } | ||
col.beforeRemoveListeners.add('settings', (q, req) => { | ||
col.beforeRemoveListeners.add('settings', async (q, req) => { | ||
if (req && req.context && req.context.skipAuthorization) { | ||
@@ -89,3 +93,5 @@ return | ||
if (req && req.context && req.context.user && !req.context.user.isAdmin) { | ||
const isAdmin = await authentication.isUserAdmin(req?.context?.user, req) | ||
if (req && req.context && req.context.user && !isAdmin) { | ||
throw authorization.createAuthorizationError(col.name) | ||
@@ -92,0 +98,0 @@ } |
@@ -143,2 +143,6 @@ /*! | ||
const initFn = async (getTopLevelFunctions, compileScript) => { | ||
if (reporter.options.trustUserCode === false) { | ||
return null | ||
} | ||
if (systemHelpersCache != null) { | ||
@@ -263,2 +267,20 @@ return systemHelpersCache | ||
let helpersStr = normalizedHelpers | ||
if (reporter.options.trustUserCode === false) { | ||
const registerResults = await reporter.registerHelpersListeners.fire() | ||
const systemHelpers = [] | ||
for (const result of registerResults) { | ||
if (result == null) { | ||
continue | ||
} | ||
if (typeof result === 'string') { | ||
systemHelpers.push(result) | ||
} | ||
} | ||
const systemHelpersStr = systemHelpers.join('\n') | ||
helpersStr = normalizedHelpers + '\n' + systemHelpersStr | ||
} | ||
try { | ||
@@ -269,3 +291,3 @@ return await reporter.runInSandbox({ | ||
}, | ||
userCode: normalizedHelpers, | ||
userCode: helpersStr, | ||
initFn, | ||
@@ -272,0 +294,0 @@ executionFn, |
@@ -22,2 +22,3 @@ const ExtensionsManager = require('./extensionsManager') | ||
this._initialized = false | ||
this._lockedDown = false | ||
this._documentStoreData = documentStore | ||
@@ -84,2 +85,46 @@ this._requestContextMetaConfigCollection = new Map() | ||
if (!this._lockedDown && this.options.trustUserCode === false) { | ||
require('@jsreport/ses') | ||
// eslint-disable-next-line | ||
lockdown({ | ||
// don't change locale based methods which users may be using in their templates | ||
localeTaming: 'unsafe', | ||
errorTaming: 'unsafe', | ||
stackFiltering: 'verbose', | ||
/* | ||
FROM SES DOCS | ||
The 'severe' setting enables all the properties on at least Object.prototype, which is sometimes needed for compatibility with code generated by rollup or webpack. | ||
However, this extra compatibility comes at the price of a miserable debugging experience. | ||
We need this to make jsrender working, which overrides constructor. | ||
In case we need to put back default, we will need to fork jsrender and change the following line | ||
(Tag.prototype = compiledDef).constructor = compiledDef._ctr = Tag; | ||
x | ||
Tag.prototype = compiledDef | ||
compiledDef._ctr = Tag | ||
*/ | ||
overrideTaming: 'severe' | ||
}) | ||
// in this mode we alias the unsafe methods to safe ones | ||
Buffer.allocUnsafe = function allocUnsafe (size) { | ||
return Buffer.alloc(size) | ||
} | ||
Buffer.allocUnsafeSlow = function allocUnsafeSlow (size) { | ||
return Buffer.alloc(size) | ||
} | ||
// we also harden Buffer because we expose it to sandbox | ||
// eslint-disable-next-line | ||
harden(Buffer) | ||
// we need to expose Intl to sandbox | ||
// eslint-disable-next-line | ||
harden(Intl) | ||
this._lockedDown = true | ||
} | ||
this._initialized = true | ||
@@ -86,0 +131,0 @@ } |
const util = require('util') | ||
const { VM, VMScript } = require('vm2') | ||
const originalVM = require('vm') | ||
const vm = require('vm') | ||
const stackTrace = require('stack-trace') | ||
@@ -9,3 +8,3 @@ const { codeFrameColumns } = require('@babel/code-frame') | ||
module.exports = function createSandbox (_sandbox, options = {}) { | ||
module.exports = async function createSandbox (_sandbox, options = {}) { | ||
const { | ||
@@ -51,7 +50,31 @@ rootDirectory, | ||
let safeVM | ||
// with standard vm this variable is the same as context, with vm2 it is a proxy of context | ||
// (which is not the real internal context) | ||
const sourceFilesInfo = new Map() | ||
// eslint-disable-next-line | ||
let compartment | ||
if (safeExecution) { | ||
// eslint-disable-next-line | ||
compartment = new Compartment() | ||
} | ||
let vmSandbox | ||
if (safeExecution) { | ||
vmSandbox = compartment.globalThis | ||
vmSandbox = Object.assign(vmSandbox, { | ||
// SES does not expose the Buffer, Intl by default, we expose it because it is handy for users, | ||
// it is exposed as it is, because we already harden() it on reporter init | ||
Buffer, | ||
Intl, | ||
// we need to expose Date, and Math to allow Date.now(), Math.random() | ||
// these objects are already hardened by lockdown() | ||
Date, | ||
Math | ||
}) | ||
} else { | ||
vmSandbox = vm.createContext(undefined) | ||
vmSandbox.Buffer = Buffer | ||
} | ||
const doSandboxRequire = createSandboxRequire(safeExecution, isolateModules, modulesCache, { | ||
@@ -68,39 +91,13 @@ rootDirectory, | ||
console: _console, | ||
require (m) { return doSandboxRequire(m, { context: vmSandbox }) } | ||
require: (m) => { return doSandboxRequire(m, { context: vmSandbox }) }, | ||
setTimeout: (...args) => { | ||
return setTimeout(...args) | ||
}, | ||
clearTimeout: (...args) => { | ||
return clearTimeout(...args) | ||
} | ||
}) | ||
if (safeExecution) { | ||
safeVM = new VM() | ||
// delete the vm.sandbox.global because it introduces json stringify issues | ||
// and we don't need such global in context | ||
delete safeVM.sandbox.global | ||
for (const name in sandbox) { | ||
safeVM.setGlobal(name, sandbox[name]) | ||
} | ||
// so far we don't have the need to have access to real vm context inside vm2, | ||
// but if we need it, we should use the code bellow to get it. | ||
// NOTE: if we need to upgrade vm2 we will need to check the source of this function | ||
// in vm2 repo and see if we need to change this, | ||
// we just execute this to get access to the internal context, so we can use it later | ||
// with the our require function, in newer versions of vm2 we may need to change how to | ||
// get access to it | ||
// https://github.com/patriksimek/vm2/blob/3.9.17/lib/vm.js#L281 | ||
// safeVM._runScript({ | ||
// runInContext: (_context) => { | ||
// vmContext = _context | ||
// return '' | ||
// } | ||
// }) | ||
vmSandbox = safeVM.sandbox | ||
} else { | ||
vmSandbox = originalVM.createContext(undefined) | ||
vmSandbox.Buffer = Buffer | ||
for (const name in sandbox) { | ||
vmSandbox[name] = sandbox[name] | ||
} | ||
for (const name in sandbox) { | ||
vmSandbox[name] = sandbox[name] | ||
} | ||
@@ -118,4 +115,2 @@ | ||
const sourceFilesInfo = new Map() | ||
return { | ||
@@ -135,4 +130,2 @@ sandbox: vmSandbox, | ||
async run (codeOrScript, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) { | ||
let runScript | ||
if (filename != null && source != null) { | ||
@@ -142,19 +135,12 @@ sourceFilesInfo.set(filename, { filename, source, entity, entitySet, errorLineNumberOffset }) | ||
const script = typeof codeOrScript !== 'string' ? codeOrScript : doCompileScript(codeOrScript, filename, safeExecution) | ||
if (safeExecution) { | ||
runScript = async function runScript () { | ||
return safeVM.run(script) | ||
try { | ||
if (safeExecution) { | ||
return await compartment.evaluate(codeOrScript + `\n//# sourceURL=${filename}`) | ||
} | ||
} else { | ||
runScript = async function runScript () { | ||
return script.runInContext(vmSandbox, { | ||
displayErrors: true | ||
}) | ||
} | ||
} | ||
try { | ||
const result = await runScript() | ||
return result | ||
const script = typeof codeOrScript !== 'string' ? codeOrScript : doCompileScript(codeOrScript, filename, safeExecution) | ||
return await script.runInContext(vmSandbox, { | ||
displayErrors: true | ||
}) | ||
} catch (e) { | ||
@@ -170,43 +156,15 @@ decorateErrorMessage(e, sourceFilesInfo) | ||
function doCompileScript (code, filename, safeExecution) { | ||
let script | ||
if (safeExecution) { | ||
script = new VMScript(code, filename) | ||
return code | ||
} | ||
// NOTE: if we need to upgrade vm2 we will need to check the source of this function | ||
// in vm2 repo and see if we need to change this, | ||
// we needed to override this method because we want "displayErrors" to be true in order | ||
// to show nice error when the compile of a script fails | ||
// https://github.com/patriksimek/vm2/blob/3.9.17/lib/script.js#L329 | ||
script._compile = function (prefix, suffix) { | ||
return new originalVM.Script(prefix + this.getCompiledCode() + suffix, { | ||
__proto__: null, | ||
filename: this.filename, | ||
displayErrors: true, | ||
lineOffset: this.lineOffset, | ||
columnOffset: this.columnOffset, | ||
// THIS FN WAS TAKEN FROM vm2 source, nothing special here | ||
importModuleDynamically: () => { | ||
// 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. | ||
// eslint-disable-next-line no-throw-literal | ||
throw 'Dynamic imports are not allowed.' | ||
} | ||
}) | ||
return new vm.Script(code, { | ||
filename, | ||
displayErrors: true, | ||
importModuleDynamically: () => { | ||
// 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. | ||
// eslint-disable-next-line no-throw-literal | ||
throw 'Dynamic imports are not allowed.' | ||
} | ||
// do the compilation | ||
script._compileVM() | ||
} else { | ||
script = new originalVM.Script(code, { | ||
filename, | ||
displayErrors: true, | ||
importModuleDynamically: () => { | ||
// 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. | ||
// eslint-disable-next-line no-throw-literal | ||
throw 'Dynamic imports are not allowed.' | ||
} | ||
}) | ||
} | ||
return script | ||
}) | ||
} | ||
@@ -213,0 +171,0 @@ |
@@ -9,4 +9,2 @@ const Module = require('module') | ||
let ALL_BUILTIN_MODULES | ||
// The isolated require is a function that replicates the node.js require but that does not | ||
@@ -28,3 +26,3 @@ // cache the modules with the standard node.js cache, instead its uses its own cache in order | ||
if (isBuiltinModule(moduleId)) { | ||
if (Module.isBuiltin(moduleId)) { | ||
// built-in modules can not be require from other part than the node.js require | ||
@@ -179,20 +177,2 @@ // perhaps in the future it can be possible: | ||
// NOTE: we can not use Module.isBuiltin because it is not available on node 16 | ||
// we can upgrade our implementation to just use Module.isBuiltin when we drop support for node 16 | ||
// https://github.com/nodejs/node/blob/v18.14.2/lib/internal/modules/cjs/loader.js#L252 | ||
function isBuiltinModule (moduleId) { | ||
// use the standard function when available | ||
if (Module.isBuiltin) { | ||
return Module.isBuiltin(moduleId) | ||
} | ||
// the only version in which this code would run is node 16 and early versions of node 18 | ||
// https://github.com/nodejs/node/blob/v18.14.2/lib/internal/modules/cjs/loader.js#L252 | ||
if (!ALL_BUILTIN_MODULES) { | ||
ALL_BUILTIN_MODULES = new Set(Module.builtinModules.flatMap((bm) => [bm, `node:${bm}`])) | ||
} | ||
return ALL_BUILTIN_MODULES.has(moduleId) | ||
} | ||
// https://github.com/nodejs/node/blob/v18.14.2/lib/internal/modules/cjs/helpers.js#L65 | ||
@@ -199,0 +179,0 @@ function makeRequireFunction (mod, requireFromRootDirectory, currentExtensions) { |
@@ -24,3 +24,3 @@ const LRU = require('lru-cache') | ||
// we use dynamic name because of the potential nested vm2 execution in the jsreportProxy.assets.require | ||
// we use dynamic name because of the potential nested vm execution in the jsreportProxy.assets.require | ||
// it may turn out it is a bad approach in assets so we gonna delete it here | ||
@@ -33,7 +33,6 @@ const executionFnName = `${nanoid()}_executionFn` | ||
context.__parentModuleDirectory = reporter.options.parentModuleDirectory | ||
context.setTimeout = setTimeout | ||
context.__topLevelFunctions = {} | ||
context.__handleError = (err) => handleError(reporter, err) | ||
const { sourceFilesInfo, run, compileScript, restore, sandbox, sandboxRequire } = createSandbox(context, { | ||
const { sourceFilesInfo, run, compileScript, restore, sandbox, sandboxRequire } = await createSandbox(context, { | ||
rootDirectory: reporter.options.rootDirectory, | ||
@@ -164,5 +163,19 @@ onLog: (log) => { | ||
const functionNames = getTopLevelFunctions(functionsCache, userCode) | ||
const functionsCode = `return {${functionNames.map(h => `"${h}": ${h}`).join(',')}}` | ||
const executionCode = `;(async () => { ${userCode} \n\n;${functionsCode} })() | ||
.then((topLevelFunctions) => { | ||
// it is better we remove our internal functions so we avoid user having the chance | ||
// to call them, as long as we force the execution to be truly async (with the await 1) | ||
// then it is safe to delete __handleError from context, when the execution is truly | ||
// async then it means the __handleError was already passed to catch handler, | ||
// therefore safe to delete | ||
const contextNormalizeCode = [ | ||
'await 1;', | ||
`const ${executionFnName}_expose = ${executionFnName};`, | ||
'delete this.__handleError;', | ||
`delete this['${executionFnName}'];` | ||
].join('') | ||
const functionsCode = `return {topLevelFunctions: {${functionNames.map(h => `"${h}": ${h}`).join(',')}}, fnToExecute: ${executionFnName}_expose}` | ||
const executionCode = `;(async () => { ${contextNormalizeCode}${userCode} \n\n;${functionsCode} })() | ||
.then(({ topLevelFunctions, fnToExecute }) => { | ||
const mergedTopLevelFunctions = { ...topLevelFunctions, ...__topLevelFunctions } | ||
@@ -176,3 +189,3 @@ | ||
return ${executionFnName}({ | ||
return fnToExecute({ | ||
topLevelFunctions: mergedTopLevelFunctions, | ||
@@ -179,0 +192,0 @@ require, |
{ | ||
"name": "@jsreport/jsreport-core", | ||
"version": "3.12.0", | ||
"version": "4.0.0", | ||
"description": "javascript based business reporting", | ||
@@ -43,5 +43,7 @@ "keywords": [ | ||
"@colors/colors": "1.5.0", | ||
"@jsreport/advanced-workers": "1.3.0", | ||
"@jsreport/advanced-workers": "2.0.0", | ||
"@jsreport/mingo": "2.4.1", | ||
"@jsreport/reap": "0.1.0", | ||
"@jsreport/serializator": "1.0.0", | ||
"@jsreport/ses": "1.0.0", | ||
"ajv": "6.12.6", | ||
@@ -69,4 +71,3 @@ "app-root-path": "3.0.0", | ||
"node.extend.without.arrays": "1.1.6", | ||
"semver": "7.3.5", | ||
"serializator": "1.0.2", | ||
"semver": "7.5.4", | ||
"stack-trace": "0.0.10", | ||
@@ -76,3 +77,2 @@ "triple-beam": "1.3.0", | ||
"uuid": "8.3.2", | ||
"vm2": "3.9.19", | ||
"winston": "3.8.1", | ||
@@ -91,3 +91,3 @@ "winston-transport": "4.5.0", | ||
"engines": { | ||
"node": ">=16.11" | ||
"node": ">=18.15" | ||
}, | ||
@@ -94,0 +94,0 @@ "standard": { |
@@ -285,2 +285,8 @@ # @jsreport/jsreport-core | ||
### 4.0.0 | ||
- remove old migration options `migrateXlsxTemplatesToAssets`, `migrateResourcesToAssets` | ||
- sandbox now uses SES instead of vm2 for evaluating user code | ||
- internal changes to support multi admin users | ||
### 3.12.0 | ||
@@ -287,0 +293,0 @@ |
@@ -51,2 +51,13 @@ | ||
}) | ||
let afterTemplatingEnginesExecutedEval | ||
reporter.tests.afterTemplatingEnginesExecutedEval = (fn) => { | ||
afterTemplatingEnginesExecutedEval = fn | ||
} | ||
reporter.registerMainAction('test-afterTemplatingEnginesExecutedEval', async (data, req) => { | ||
if (afterTemplatingEnginesExecutedEval == null) { | ||
return | ||
} | ||
return afterTemplatingEnginesExecutedEval.toString() | ||
}) | ||
} |
@@ -73,3 +73,10 @@ const extend = require('node.extend.without.arrays') | ||
}) | ||
reporter.afterTemplatingEnginesExecutedListeners.add('eval-listeners', async (req, res) => { | ||
const code = await reporter.executeMainAction('test-afterTemplatingEnginesExecutedEval', {}, req) | ||
if (code) { | ||
return evalInWorker(code, req, res) | ||
} | ||
}) | ||
}) | ||
} |
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 6 instances in 1 package
409
28
385279
87
9351
+ Added@jsreport/serializator@1.0.0
+ Added@jsreport/ses@1.0.0
+ Added@endo/env-options@0.1.4(transitive)
+ Added@jsreport/advanced-workers@2.0.0(transitive)
+ Added@jsreport/serializator@1.0.0(transitive)
+ Added@jsreport/ses@1.0.0(transitive)
+ Addedsemver@7.5.4(transitive)
+ Addedses@0.18.7(transitive)
- Removedserializator@1.0.2
- Removedvm2@3.9.19
- Removed@jsreport/advanced-workers@1.3.0(transitive)
- Removedacorn@8.14.0(transitive)
- Removedacorn-walk@8.3.4(transitive)
- Removedsemver@7.3.5(transitive)
- Removedserializator@1.0.2(transitive)
- Removedvm2@3.9.19(transitive)
Updatedsemver@7.5.4