Merge pull request #13027 from Budibase/enable-sloppy-js

Enable non-strict js
This commit is contained in:
Michael Drury 2024-02-19 17:44:57 +00:00 committed by GitHub
commit 656897c492
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 65 additions and 105 deletions

View File

@ -13,8 +13,8 @@
"build": "node ./scripts/build.js", "build": "node ./scripts/build.js",
"postbuild": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && copyfiles -f ../../yarn.lock ./dist/", "postbuild": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && copyfiles -f ../../yarn.lock ./dist/",
"check:types": "tsc -p tsconfig.json --noEmit --paths null", "check:types": "tsc -p tsconfig.json --noEmit --paths null",
"build:isolated-vm-lib:string-templates": "esbuild --minify --bundle src/jsRunner/bundles/index-helpers.ts --outfile=src/jsRunner/bundles/index-helpers.ivm.bundle.js --platform=node --format=esm --external:handlebars", "build:isolated-vm-lib:string-templates": "esbuild --minify --bundle src/jsRunner/bundles/index-helpers.ts --outfile=src/jsRunner/bundles/index-helpers.ivm.bundle.js --platform=node --format=iife --external:handlebars --global-name=helpers",
"build:isolated-vm-lib:bson": "esbuild --minify --bundle src/jsRunner/bundles/bsonPackage.ts --outfile=src/jsRunner/bundles/bson.ivm.bundle.js --platform=node --format=esm", "build:isolated-vm-lib:bson": "esbuild --minify --bundle src/jsRunner/bundles/bsonPackage.ts --outfile=src/jsRunner/bundles/bson.ivm.bundle.js --platform=node --format=iife --global-name=bson",
"build:isolated-vm-libs": "yarn build:isolated-vm-lib:string-templates && yarn build:isolated-vm-lib:bson", "build:isolated-vm-libs": "yarn build:isolated-vm-lib:string-templates && yarn build:isolated-vm-lib:bson",
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js", "debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,2 @@
import { EJSON } from "bson" export const deserialize = require("bson").deserialize
export const toJson = require("bson").EJSON.deserialize
export { deserialize } from "bson"
export const toJson = EJSON.deserialize

File diff suppressed because one or more lines are too long

View File

@ -2,9 +2,8 @@ const {
getJsHelperList, getJsHelperList,
} = require("../../../../string-templates/src/helpers/list.js") } = require("../../../../string-templates/src/helpers/list.js")
const helpers = getJsHelperList()
export default { export default {
...helpers, ...getJsHelperList(),
// pointing stripProtocol to a unexisting function to be able to declare it on isolated-vm // pointing stripProtocol to a unexisting function to be able to declare it on isolated-vm
// @ts-ignore // @ts-ignore
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef

View File

@ -7,22 +7,19 @@ export const enum BundleType {
BSON = "bson", BSON = "bson",
} }
const bundleSourceCode = { const bundleSourceFile: Record<BundleType, string> = {
[BundleType.HELPERS]: "../bundles/index-helpers.ivm.bundle.js", [BundleType.HELPERS]: "./index-helpers.ivm.bundle.js",
[BundleType.BSON]: "../bundles/bson.ivm.bundle.js", [BundleType.BSON]: "./bson.ivm.bundle.js",
} }
const bundleSourceCode: Partial<Record<BundleType, string>> = {}
export function loadBundle(type: BundleType) { export function loadBundle(type: BundleType) {
if (environment.isJest()) { let sourceCode = bundleSourceCode[type]
return fs.readFileSync(require.resolve(bundleSourceCode[type]), "utf-8") if (sourceCode) {
return sourceCode
} }
switch (type) { sourceCode = fs.readFileSync(require.resolve(bundleSourceFile[type]), "utf-8")
case BundleType.HELPERS: bundleSourceCode[type] = sourceCode
return require("../bundles/index-helpers.ivm.bundle.js") return sourceCode
case BundleType.BSON:
return require("../bundles/bson.ivm.bundle.js")
default:
utils.unreachable(type)
}
} }

View File

@ -42,6 +42,11 @@ describe.each([
expect(output).toBe(3) expect(output).toBe(3)
}) })
it("it can execute sloppy javascript", async () => {
const output = await processJS(`a=2;b=3;return a + b`)
expect(output).toBe(5)
})
it("should prevent sandbox escape", async () => { it("should prevent sandbox escape", async () => {
const output = await processJS( const output = await processJS(
`return this.constructor.constructor("return process.env")()` `return this.constructor.constructor("return process.env")()`

View File

@ -16,35 +16,6 @@ class ExecutionTimeoutError extends Error {
} }
} }
class ModuleHandler {
private modules: {
import: string
moduleKey: string
module: ivm.Module
}[] = []
private generateRandomKey = () => `i${crypto.randomUUID().replace(/-/g, "")}`
registerModule(module: ivm.Module, imports: string) {
this.modules.push({
moduleKey: this.generateRandomKey(),
import: imports,
module: module,
})
}
generateImports() {
return this.modules
.map(m => `import ${m.import} from "${m.moduleKey}"`)
.join(";")
}
getModule(key: string) {
const module = this.modules.find(m => m.moduleKey === key)
return module?.module
}
}
export class IsolatedVM implements VM { export class IsolatedVM implements VM {
private isolate: ivm.Isolate private isolate: ivm.Isolate
private vm: ivm.Context private vm: ivm.Context
@ -55,9 +26,8 @@ export class IsolatedVM implements VM {
// By default the wrapper returns itself // By default the wrapper returns itself
private codeWrapper: (code: string) => string = code => code private codeWrapper: (code: string) => string = code => code
private moduleHandler = new ModuleHandler()
private readonly resultKey = "results" private readonly resultKey = "results"
private runResultKey: string
constructor({ constructor({
memoryLimit, memoryLimit,
@ -76,8 +46,9 @@ export class IsolatedVM implements VM {
this.jail = this.vm.global this.jail = this.vm.global
this.jail.setSync("global", this.jail.derefInto()) this.jail.setSync("global", this.jail.derefInto())
this.runResultKey = crypto.randomUUID()
this.addToContext({ this.addToContext({
[this.resultKey]: { out: "" }, [this.resultKey]: { [this.runResultKey]: "" },
}) })
this.invocationTimeout = invocationTimeout this.invocationTimeout = invocationTimeout
@ -94,6 +65,10 @@ export class IsolatedVM implements VM {
escape: querystring.escape, escape: querystring.escape,
}) })
const cryptoModule = this.registerCallbacks({
randomUUID: crypto.randomUUID,
})
this.addToContext({ this.addToContext({
helpersStripProtocol: new ivm.Callback((str: string) => { helpersStripProtocol: new ivm.Callback((str: string) => {
var parsed = url.parse(str) as any var parsed = url.parse(str) as any
@ -102,34 +77,23 @@ export class IsolatedVM implements VM {
}), }),
}) })
const injectedRequire = `const require=function req(val) { const injectedRequire = `require=function req(val) {
switch (val) { switch (val) {
case "url": return ${urlModule}; case "url": return ${urlModule};
case "querystring": return ${querystringModule}; case "querystring": return ${querystringModule};
case "crypto": return ${cryptoModule};
} }
}` }`
const helpersSource = loadBundle(BundleType.HELPERS) const helpersSource = loadBundle(BundleType.HELPERS)
const helpersModule = this.isolate.compileModuleSync( const script = this.isolate.compileScriptSync(
`${injectedRequire};${helpersSource}` `${injectedRequire};${helpersSource};helpers=helpers.default`
) )
helpersModule.instantiateSync(this.vm, specifier => { script.runSync(this.vm, { timeout: this.invocationTimeout, release: false })
if (specifier === "crypto") { new Promise(() => {
const cryptoModule = this.registerCallbacks({ script.release()
randomUUID: crypto.randomUUID,
})
const module = this.isolate.compileModuleSync(
`export default ${cryptoModule}`
)
module.instantiateSync(this.vm, specifier => {
throw new Error(`No imports allowed. Required: ${specifier}`)
})
return module
}
throw new Error(`No imports allowed. Required: ${specifier}`)
}) })
this.moduleHandler.registerModule(helpersModule, "helpers")
return this return this
} }
@ -151,9 +115,9 @@ export class IsolatedVM implements VM {
// 4. Stringify the result in order to convert the result from BSON to json // 4. Stringify the result in order to convert the result from BSON to json
this.codeWrapper = code => this.codeWrapper = code =>
`(function(){ `(function(){
const data = deserialize(bsonData, { validation: { utf8: false } }).data; const data = bson.deserialize(bsonData, { validation: { utf8: false } }).data;
const result = ${code} const result = ${code}
return toJson(result); return bson.toJson(result);
})();` })();`
const bsonSource = loadBundle(BundleType.BSON) const bsonSource = loadBundle(BundleType.BSON)
@ -173,7 +137,7 @@ export class IsolatedVM implements VM {
}) })
// "Polyfilling" text decoder. `bson.deserialize` requires decoding. We are creating a bridge function so we don't need to inject the full library // "Polyfilling" text decoder. `bson.deserialize` requires decoding. We are creating a bridge function so we don't need to inject the full library
const textDecoderPolyfill = class TextDecoder { const textDecoderPolyfill = class TextDecoderMock {
constructorArgs constructorArgs
constructor(...constructorArgs: any) { constructor(...constructorArgs: any) {
@ -187,16 +151,18 @@ export class IsolatedVM implements VM {
functionArgs: input, functionArgs: input,
}) })
} }
}.toString() }
const bsonModule = this.isolate.compileModuleSync( .toString()
.replace(/TextDecoderMock/, "TextDecoder")
const script = this.isolate.compileScriptSync(
`${textDecoderPolyfill};${bsonSource}` `${textDecoderPolyfill};${bsonSource}`
) )
bsonModule.instantiateSync(this.vm, specifier => { script.runSync(this.vm, { timeout: this.invocationTimeout, release: false })
throw new Error(`No imports allowed. Required: ${specifier}`) new Promise(() => {
script.release()
}) })
this.moduleHandler.registerModule(bsonModule, "{deserialize, toJson}")
return this return this
} }
@ -210,25 +176,18 @@ export class IsolatedVM implements VM {
} }
} }
code = `${this.moduleHandler.generateImports()};results.out=${this.codeWrapper( code = `results['${this.runResultKey}']=${this.codeWrapper(code)}`
code
)};`
const script = this.isolate.compileModuleSync(code) const script = this.isolate.compileScriptSync(code)
script.instantiateSync(this.vm, specifier => { script.runSync(this.vm, { timeout: this.invocationTimeout, release: false })
const module = this.moduleHandler.getModule(specifier) new Promise(() => {
if (module) { script.release()
return module
}
throw new Error(`"${specifier}" import not allowed`)
}) })
script.evaluateSync({ timeout: this.invocationTimeout }) // We can't rely on the script run result as it will not work for non-transferable values
const result = this.getFromContext(this.resultKey) const result = this.getFromContext(this.resultKey)
return result.out return result[this.runResultKey]
} }
private registerCallbacks(functions: Record<string, any>) { private registerCallbacks(functions: Record<string, any>) {
@ -268,7 +227,10 @@ export class IsolatedVM implements VM {
private getFromContext(key: string) { private getFromContext(key: string) {
const ref = this.vm.global.getSync(key, { reference: true }) const ref = this.vm.global.getSync(key, { reference: true })
const result = ref.copySync() const result = ref.copySync()
ref.release()
new Promise(() => {
ref.release()
})
return result return result
} }
} }

View File

@ -49,7 +49,6 @@ function runBuild(entry, outfile) {
preserveSymlinks: true, preserveSymlinks: true,
loader: { loader: {
".svelte": "copy", ".svelte": "copy",
".ivm.bundle.js": "text",
}, },
metafile: true, metafile: true,
external: [ external: [
@ -70,7 +69,7 @@ function runBuild(entry, outfile) {
platform: "node", platform: "node",
outfile, outfile,
}).then(result => { }).then(result => {
glob(`${process.cwd()}/src/**/*.hbs`, {}, (err, files) => { glob(`${process.cwd()}/src/**/*.{hbs,ivm.bundle.js}`, {}, (err, files) => {
for (const file of files) { for (const file of files) {
fs.copyFileSync(file, `${process.cwd()}/dist/${path.basename(file)}`) fs.copyFileSync(file, `${process.cwd()}/dist/${path.basename(file)}`)
} }