This might be a bit of a specific problem (research project), but I'm looking for a good way to make this program significantly "safer" than it currently is.
The goal is to accept arbitrary user code (after it has passed unit testing and a manual inspection) and incorporate it into a running program without the need to reload the application.
The specific example is that it is a drawing application that continuously executes and I would like students/other users to be able to commit a script (that follows a specific format/guideline).
My current implementation is to require that the user specify that their classname match their filename (e.g., myclass.js
has a const myclass = class { ... }
block inside). Then when I see that a new file exists within my server (a Flask application that checks the contents of a particular directory), the JavaScript application will load that particular file.
At present I have a promise
in place to ensure the script is loaded, but the general method I'm doing is this to bring it into memory:
function loadNewScript(scriptName) {
let s = document.createElement("script");
s.setAttribute("type", "text/javascript");
s.setAttribute("src", `/static/techniques/${scriptName}`);
let nodes = document.getElementsByTagName("*");
let node = nodes[nodes.length - 1].parentNode;
node.appendChild(s);
}
However, the security issue I'm anticipating (other than expecting arbitrary code I suppose) is that I'm currently using eval
to bring the class into memory:
// technique comes in as 'myclass.js'
function loadObject(technique) {
try {
let obj = eval(technique.split(".")[0]);
let _activeObj = new obj();
if (typeof _activeObj != "undefined") {
return _activeObj;
}
return null;
}
catch (e) { // will check for syntax errors, but this usually trips when the script isn't loaded yet
return null;
}
}
At present this works, however I'm a bit concerned about the use of eval
here. I've seen posts regarding using the window
or this
namespace, but from my understanding you can't add a dynamically-loaded script into the global namespace? At any rate, using window['myclass']
or this['myclass']
isn't working after it is loaded. (How to execute a JavaScript function when I have its name as a string)
No point in avoiding eval
for security, really, if you already load "arbitrary" (reviewed) code into dynamic <script>
s. The problem why window['myclass']
does not work is because const
does declare a global variable but create a property of the global object. You'd need to change the scripts to use var myclass = class { ... };
instead.
However, I would recommend you change your implementation to require files that follow the ES6 module format:
export default class myclass { ... }
and then you can load these using
function loadNewScript(scriptName) {
return import(`/static/techniques/${scriptName}`);
}
async function loadObject(technique) {
const module = await loadNewScript(technique);
const obj = module.default;
return new obj();
}
If you need the loadObject
method to be synchronous (instead of returning a promise), you can use an object or Map
as a class registry:
const registry = {};
async function loadNewScript(scriptName) {
const module = await import(`/static/techniques/${scriptName}`);
registry[scriptName] = module.default;
}
function loadObject(technique) {
const obj = registry[technique];
return obj ? new obj() : null;
}