relative to #955 , i want a sandboxed python runtime, i face two question is that :
- how to limit the
import
function ? have any offical way todo this work ? i want limit the import with a denyList or a allowList, like the js
i will deny it in a special pyodide.runPythonAsync
call.
- how to pip out the stdio ( redirect the
print
output from python ) ? now i use a way is make a patch in the sys.stdout
from here, but seems like it will failure after i import the numpy
package , so strange .
about the question (1) , i was try 2 way to do. ( sorry i never read the code of the pyodide , all the follow patch work are try on runtime console and then write to code . )
Summary: i run the pyodide in WebWorker to use the javascript sandboxie runtime to have a additional protection .
1. let the debuger hard to direct find the pyodide ref from root ( Worker's self
or globalThis
)
- in one side, i wrapper all the thing into a Typescript class , i named it
class SClass
( mean : secure class)
use follow code to hide it
class SClass {
constructor() {
workerSelf.onmessage = this.onmessage.bind(this);
}
// .......
const workerSelf: typeof WorkerSelf = self as any;
workerSelf.aaaaaa = new SClass();
workerSelf.aaaaaa.init();
// https://github.com/Microsoft/TypeScript/issues/24587#issuecomment-412287117
// we have the `workerSelf.onmessage` on the ref link, the `onmessage` are `onmessage.bind(this)`,
// so, the GC couldn't delete our object,
// so, we can safe delete the direct ref from root object,
// then, we implement the hidden all the ref target,
// no one can access our object from debug console.
// @ts-ignore
// workerSelf[Symbol() as any] = workerSelf.aaaaaa;
delete workerSelf.aaaaaa;
now , no one can access the SClass from self
.
- in other side, because the pyodide cannot be delete from
globalThis
, (it be set the non-configable
flag), i cloned all the field and delete them from pyodide, only recover the pyodide._module
to let it can use import js
init() {
return workerSelf.languagePluginLoader.then(async () => {
// await workerSelf.pyodide.loadPackage(['numpy', 'pytz']);
// delete root ref from self.root
this.pyodide = clone(workerSelf.pyodide);
// console.log(this.pyodide);
// console.log(this.pyodide._module);
// console.log(Object.getOwnPropertyNames(workerSelf.pyodide));
Object.keys(workerSelf.pyodide).forEach(T => delete workerSelf.pyodide[T]);
// delete packages lookup-table from pyodide
// console.log('packages', workerSelf.pyodide._module.packages);
// console.log('packages', this.pyodide._module.packages);
workerSelf.pyodide._module = {};
workerSelf.pyodide._module.packages = this.pyodide._module.packages;
workerSelf.pyodide._module.packages = {
dependencies: {},
import_name_to_package_name: {},
};
// console.log('packages', workerSelf.pyodide._module.packages);
})
now, other than the js
package cannot be import, all the try will throw error .
.
but if i only want a allowList or denyList, i cannot comlete it use follow code to recover the pyodide._module.packages
. it's not work.
// recover selected packages to pyodide lookup-table
const recoverPack = (pName: string) => {
workerSelf.pyodide._module.packages.dependencies[pName] =
this.pyodide._module.packages.dependencies[pName];
workerSelf.pyodide._module.packages.import_name_to_package_name[pName] =
this.pyodide._module.packages.import_name_to_package_name[pName];
};
const allowList = [
'numpy',
];
allowList.forEach(T => recoverPack(T));
Object.getOwnPropertyNames(this.pyodide._module.packages.dependencies).forEach(T => recoverPack(T));
// dont remember to recover the prototype
if (allowList.length > 0) {
// console.log('this.pyodide._module.packages.prototype',
// Object.getPrototypeOf(this.pyodide._module.packages));
Object.setPrototypeOf(workerSelf.pyodide._module.packages,
Object.getPrototypeOf(this.pyodide._module.packages));
Object.setPrototypeOf(workerSelf.pyodide._module.packages.dependencies,
Object.getPrototypeOf(this.pyodide._module.packages.dependencies));
Object.setPrototypeOf(workerSelf.pyodide._module.packages.import_name_to_package_name,
Object.getPrototypeOf(this.pyodide._module.packages.import_name_to_package_name));
}
console.log('packages', workerSelf.pyodide._module.packages);
seems like some other important thing i deleted from the workerSelf.pyodide
.
in the end, i remove above the attempt that delete thing from pyodide.
2. use pyodide.find_imports
to check the code before run it . and simple not run it if find something not allowed.
see pyodide.find_imports
thsi is a easy way, but in the document i see some dangerous function can bypass this way. like pyodide.eval_code
or the pyodide.open_url
, and i not sure not have any other way can load code from remote or eval a string to code in runtime.
BTW: the cursor
params of pyodide.get_completions
API seems like not receive {row, col} as params, i dont known how to use it in a large section code. so, now i run a second WebWorker to run the Jedi
to do the code complete task. but seems like the jedi
cannot get information from a imported package , like numpy
.
sorry for my knowledge, i'm good at C++/Typescript and software security/software architecture, the Python is in my weakness area.