Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
) The `vm2` module has been deprecated and has critical security vulnerabilities. The suggested replacement module `isolated-vm` is not suitable for these packages, since it relies on a C++ binary. Instead, these packages will use the `quickjs-emscripten` module to execute the user code in an isolated QuickJS environment compiled to WASM. This should allow the highest level of sandboxing and will hopefully put an end to this cat and mouse game once and for all. Fixes #218.
- Loading branch information
1 parent
f6d42a4
commit f1f3220
Showing
14 changed files
with
509 additions
and
371 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'pac-proxy-agent': major | ||
'pac-resolver': major | ||
'degenerator': major | ||
--- | ||
|
||
Use `quickjs-emscripten` instead of `vm2` to execute PAC file code |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'proxy-agent': minor | ||
--- | ||
|
||
Use QuickJS version of `pac-proxy-agent` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { types } from 'util'; | ||
import { degenerator } from './degenerator'; | ||
import type { Context } from 'vm'; | ||
import type { | ||
QuickJSContext, | ||
QuickJSHandle, | ||
QuickJSWASMModule, | ||
} from '@tootallnate/quickjs-emscripten'; | ||
import type { DegeneratorNames } from './degenerator'; | ||
|
||
export interface CompileOptions { | ||
names?: DegeneratorNames; | ||
filename?: string; | ||
sandbox?: Context; | ||
} | ||
|
||
export function compile<R = unknown, A extends unknown[] = []>( | ||
qjs: QuickJSWASMModule, | ||
code: string, | ||
returnName: string, | ||
options: CompileOptions = {} | ||
): (...args: A) => Promise<R> { | ||
const compiled = degenerator(code, options.names ?? []); | ||
|
||
const vm = qjs.newContext(); | ||
|
||
// Add functions to global | ||
if (options.sandbox) { | ||
for (const [name, value] of Object.entries(options.sandbox)) { | ||
if (typeof value !== 'function') { | ||
throw new Error( | ||
`Expected a "function" for sandbox property \`${name}\`, but got "${typeof value}"` | ||
); | ||
} | ||
const fnHandle = vm.newFunction(name, (...args) => { | ||
//console.log('invoke', { name, args }); | ||
const result = value( | ||
...args.map((arg) => quickJSHandleToHost(vm, arg)) | ||
); | ||
vm.runtime.executePendingJobs(); | ||
return hostToQuickJSHandle(vm, result); | ||
}); | ||
fnHandle.consume((handle) => vm.setProp(vm.global, name, handle)); | ||
} | ||
} | ||
|
||
//console.log(compiled); | ||
const fnResult = vm.evalCode(`${compiled};${returnName}`, options.filename); | ||
const fn = vm.unwrapResult(fnResult); | ||
|
||
const t = vm.typeof(fn); | ||
if (t !== 'function') { | ||
throw new Error( | ||
`Expected a "function" named \`${returnName}\` to be defined, but got "${t}"` | ||
); | ||
} | ||
const r = async function (...args: A): Promise<R> { | ||
let promiseHandle: QuickJSHandle | undefined; | ||
let resolvedHandle: QuickJSHandle | undefined; | ||
try { | ||
const result = vm.callFunction( | ||
fn, | ||
vm.undefined, | ||
...args.map((arg) => hostToQuickJSHandle(vm, arg)) | ||
); | ||
promiseHandle = vm.unwrapResult(result); | ||
const resolvedResultP = vm.resolvePromise(promiseHandle); | ||
vm.runtime.executePendingJobs(); | ||
const resolvedResult = await resolvedResultP; | ||
resolvedHandle = vm.unwrapResult(resolvedResult); | ||
return quickJSHandleToHost(vm, resolvedHandle); | ||
} catch (err: unknown) { | ||
if (err && typeof err === 'object' && 'cause' in err && err.cause) { | ||
if ( | ||
typeof err.cause === 'object' && | ||
'stack' in err.cause && | ||
'name' in err.cause && | ||
'message' in err.cause && | ||
typeof err.cause.stack === 'string' && | ||
typeof err.cause.name === 'string' && | ||
typeof err.cause.message === 'string' | ||
) { | ||
// QuickJS Error `stack` does not include the name + | ||
// message, so patch those in to behave more like V8 | ||
err.cause.stack = `${err.cause.name}: ${err.cause.message}\n${err.cause.stack}`; | ||
} | ||
throw err.cause; | ||
} | ||
throw err; | ||
} finally { | ||
promiseHandle?.dispose(); | ||
resolvedHandle?.dispose(); | ||
} | ||
}; | ||
Object.defineProperty(r, 'toString', { | ||
value: () => compiled, | ||
enumerable: false, | ||
}); | ||
return r; | ||
} | ||
|
||
function quickJSHandleToHost(vm: QuickJSContext, val: QuickJSHandle) { | ||
return vm.dump(val); | ||
} | ||
|
||
function hostToQuickJSHandle(vm: QuickJSContext, val: unknown): QuickJSHandle { | ||
if (typeof val === 'undefined') { | ||
return vm.undefined; | ||
} else if (val === null) { | ||
return vm.null; | ||
} else if (typeof val === 'string') { | ||
return vm.newString(val); | ||
} else if (typeof val === 'number') { | ||
return vm.newNumber(val); | ||
} else if (typeof val === 'bigint') { | ||
return vm.newBigInt(val); | ||
} else if (typeof val === 'boolean') { | ||
return val ? vm.true : vm.false; | ||
} else if (types.isPromise(val)) { | ||
const promise = vm.newPromise(); | ||
promise.settled.then(vm.runtime.executePendingJobs); | ||
val.then( | ||
(r: unknown) => { | ||
promise.resolve(hostToQuickJSHandle(vm, r)); | ||
}, | ||
(err: unknown) => { | ||
promise.reject(hostToQuickJSHandle(vm, err)); | ||
} | ||
); | ||
return promise.handle; | ||
} else if (types.isNativeError(val)) { | ||
return vm.newError(val); | ||
} | ||
throw new Error(`Unsupported value: ${val}`); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import { types } from 'util'; | ||
import { generate } from 'escodegen'; | ||
import { parseScript } from 'esprima'; | ||
import { visit, namedTypes as n, builders as b } from 'ast-types'; | ||
|
||
export type DegeneratorName = string | RegExp; | ||
export type DegeneratorNames = DegeneratorName[]; | ||
|
||
/** | ||
* Compiles sync JavaScript code into JavaScript with async Functions. | ||
* | ||
* @param {String} code JavaScript string to convert | ||
* @param {Array} names Array of function names to add `await` operators to | ||
* @return {String} Converted JavaScript string with async/await injected | ||
* @api public | ||
*/ | ||
|
||
export function degenerator(code: string, _names: DegeneratorNames): string { | ||
if (!Array.isArray(_names)) { | ||
throw new TypeError('an array of async function "names" is required'); | ||
} | ||
|
||
// Duplicate the `names` array since it's rude to augment the user args | ||
const names = _names.slice(0); | ||
|
||
const ast = parseScript(code); | ||
|
||
// First pass is to find the `function` nodes and turn them into async or | ||
// generator functions only if their body includes `CallExpressions` to | ||
// function in `names`. We also add the names of the functions to the `names` | ||
// array. We'll iterate several time, as every iteration might add new items | ||
// to the `names` array, until no new names were added in the iteration. | ||
let lastNamesLength = 0; | ||
do { | ||
lastNamesLength = names.length; | ||
visit(ast, { | ||
visitVariableDeclaration(path) { | ||
if (path.node.declarations) { | ||
for (let i = 0; i < path.node.declarations.length; i++) { | ||
const declaration = path.node.declarations[i]; | ||
if ( | ||
n.VariableDeclarator.check(declaration) && | ||
n.Identifier.check(declaration.init) && | ||
n.Identifier.check(declaration.id) && | ||
checkName(declaration.init.name, names) && | ||
!checkName(declaration.id.name, names) | ||
) { | ||
names.push(declaration.id.name); | ||
} | ||
} | ||
} | ||
return false; | ||
}, | ||
visitAssignmentExpression(path) { | ||
if ( | ||
n.Identifier.check(path.node.left) && | ||
n.Identifier.check(path.node.right) && | ||
checkName(path.node.right.name, names) && | ||
!checkName(path.node.left.name, names) | ||
) { | ||
names.push(path.node.left.name); | ||
} | ||
return false; | ||
}, | ||
visitFunction(path) { | ||
if (path.node.id) { | ||
let shouldDegenerate = false; | ||
visit(path.node, { | ||
visitCallExpression(path) { | ||
if (checkNames(path.node, names)) { | ||
shouldDegenerate = true; | ||
} | ||
return false; | ||
}, | ||
}); | ||
|
||
if (!shouldDegenerate) { | ||
return false; | ||
} | ||
|
||
// Got a "function" expression/statement, | ||
// convert it into an async function | ||
path.node.async = true; | ||
|
||
// Add function name to `names` array | ||
if (!checkName(path.node.id.name, names)) { | ||
names.push(path.node.id.name); | ||
} | ||
} | ||
|
||
this.traverse(path); | ||
}, | ||
}); | ||
} while (lastNamesLength !== names.length); | ||
|
||
// Second pass is for adding `await` statements to any function | ||
// invocations that match the given `names` array. | ||
visit(ast, { | ||
visitCallExpression(path) { | ||
if (checkNames(path.node, names)) { | ||
// A "function invocation" expression, | ||
// we need to inject an `AwaitExpression` | ||
const delegate = false; | ||
const { | ||
name, | ||
parent: { node: pNode }, | ||
} = path; | ||
|
||
const expr = b.awaitExpression(path.node, delegate); | ||
|
||
if (n.CallExpression.check(pNode)) { | ||
pNode.arguments[name] = expr; | ||
} else { | ||
pNode[name] = expr; | ||
} | ||
} | ||
|
||
this.traverse(path); | ||
}, | ||
}); | ||
|
||
return generate(ast); | ||
} | ||
|
||
/** | ||
* Returns `true` if `node` has a matching name to one of the entries in the | ||
* `names` array. | ||
* | ||
* @param {types.Node} node | ||
* @param {Array} names Array of function names to return true for | ||
* @return {Boolean} | ||
* @api private | ||
*/ | ||
|
||
function checkNames( | ||
{ callee }: n.CallExpression, | ||
names: DegeneratorNames | ||
): boolean { | ||
let name: string; | ||
if (n.Identifier.check(callee)) { | ||
name = callee.name; | ||
} else if (n.MemberExpression.check(callee)) { | ||
if ( | ||
n.Identifier.check(callee.object) && | ||
n.Identifier.check(callee.property) | ||
) { | ||
name = `${callee.object.name}.${callee.property.name}`; | ||
} else { | ||
return false; | ||
} | ||
} else if (n.FunctionExpression.check(callee)) { | ||
if (callee.id) { | ||
name = callee.id.name; | ||
} else { | ||
return false; | ||
} | ||
} else { | ||
throw new Error(`Don't know how to get name for: ${callee.type}`); | ||
} | ||
return checkName(name, names); | ||
} | ||
|
||
function checkName(name: string, names: DegeneratorNames): boolean { | ||
// now that we have the `name`, check if any entries match in the `names` array | ||
for (let i = 0; i < names.length; i++) { | ||
const n = names[i]; | ||
if (types.isRegExp(n)) { | ||
if (n.test(name)) { | ||
return true; | ||
} | ||
} else if (name === n) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} |
Oops, something went wrong.
f1f3220
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
proxy-agents – ./
proxy-agents-git-main-tootallnate.vercel.app
proxy-agents.vercel.app
proxy-agents-tootallnate.vercel.app