Search code examples
javascripteval

Restructuring dynamically-loaded scripts within JavaScript program to avoid eval


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)


Solution

  • 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;
    }