4
\$\begingroup\$

Primary Concerns

  • As long as the stash is not within scope, globals such as window/global would effectively be sanitized of any risky methods, such as eval

  • The API feels complete, am I missing anything?

  • Are there any "risky" aspects to the sever method.
  • Any thoughts with regard to performance or security.
  • I don't think there is any performance implications from maintaining a stash of the extracted methods, as they would exist already, we are just moving them around.
  • is the layout below easy to read and understand, should I document these things in the future or should I just stick to the code?

Background Info

Scalpel (s5l) is a small class to shuffle properties between objects.

Initially developed for some work being done within the Electron framework, where "window" and "global" needed to be pruned of insecure methods.

Scalpel will allow you to remove any methods from these "global" type objects, and then restore them, when/if you want.

Here is the condensed version:

class S5l /* S{calpe}(5)l*/ {
    constructor (){ this.stash = {}; }

    sever ( methods, contexts, message, stash ){
        if(! Array.isArray(methods)) methods = [methods];
        message = message ? message : ' has been disabled for security reasons.';
        this.stash = stash ? stash : this.stash;
        stash = this.stash;
        methods.forEach(function(method){
            contexts.forEach(function(tuple){
                let label = Object.keys(tuple)[0];
                if( Object.prototype.hasOwnProperty.call( tuple[label], method )){
                    stash[label] = stash[label] || {};
                    stash[label][method] = { 
                      _f: tuple[label][method], // the function/method itself.
                      _r: tuple[label], // a parent reference, where the method was.
                      _m: method // the method/property name
                    };
                    tuple[label][method] = function(){ throw new Error( method + message); };
                }
            });
        });
        return this;
    }

    purge(dotPath){
        delete this.stash;
        this.stash = {};
        return this;
    }

    _dotToSteps(dotPath){
        return dotPath.split('.');
    }

    _dotWalker(dotPath, context, _c){
        var steps = this._dotToSteps(dotPath);
        for( var i=0; i < steps.length; i++){
            if( Object.prototype.hasOwnProperty.call( context, steps[i] )  ){
                _c = context, context = context[steps[i]];
                if(i === steps.length -1) 
                    return { at: steps[i], context: context, parent: _c };
            } else {
                return undefined;
            }
        }
    }

    _retrieve(dotPath, context){
        context = context ? context : this.stash;
        return dotPath ? this._dotWalker(dotPath, context ) : context;
    }

    use(dotPath, context){
        return this._retrieve(dotPath, context).context._f || this._retrieve(dotPath, context).parent;
    }

    attach(dotPath, to, as){
        var tmp = this._retrieve(dotPath, this.stash);
            to[ as || tmp.at ] = tmp.context._f;
            return to;
    }

}

Here is the commented version:

class S5l /* S{calpe}(5)l*/ {

    constructor (){
        this.stash = {};
    }


    /**
        methods: a string or array of method names
        contexts: an tuple or array of tuples.
            tuple: { prop: object }
        message (optional): a message used to make the stub.
        stash (optional): the context used for the stash.
    */
    sever ( methods, contexts, message, stash ){
        // Ensure we have an array, from which to pull methods from.
        if(! Array.isArray(methods)) methods = [methods];

        // Prepair or Incorperate a supplied message
        message = message ? message : ' has been disabled for security reasons.';

        // Swap out stashes, or continue using this.stash
        this.stash = stash ? stash : this.stash;

        // Make link for use below, ES6, would prevent us from needing this.
        stash = this.stash;

        // Process all the method.
        methods.forEach(function(method){

            /* TODO: we should responde to all types, 
                     but for now we expect all values
                     to be objects. 
            */

            /**
                We loop through the contexts, the use case
                here is that we have multple "globals" that
                need to be stripped of a method(s).
            */
            contexts.forEach(function(tuple){
                let label = Object.keys(tuple)[0];
                // We only want to work with existing props/methods
                if( Object.prototype.hasOwnProperty.call( tuple[label], method )){

                    // Initialize a new sub object, or use existing.
                    stash[label] = stash[label] || {};

                    // Archive/Stash our source method/prop and info.
                    stash[label][method] = { 
                      _f: tuple[label][method], // the function/method itself.
                      _r: tuple[label], // a parent reference, where the method was.
                      _m: method // the method/property name
                    };

                    // Assign stub to original context property/method.
                    tuple[label][method] = function(){ throw new Error( method + message); };
                }

            });

        });

        return this;
    }
    /**
        Will drop all link/references in the stash, 
        to allow garbage collection, if applicable.
    */
    purge(dotPath){
        delete this.stash; // to ensure garbage collection
        this.stash = {}; // reinitialize store
        return this;
    }

    /** 
        Converts 'dot.path.string' into an array of successive props
    */
    _dotToSteps(dotPath){
        return dotPath.split('.');
    }

    /**
        Simple traverser, that uses the 
    */
    _dotWalker(dotPath, context, _c){
        var steps = this._dotToSteps(dotPath);
        for( var i=0; i < steps.length; i++){

            if( Object.prototype.hasOwnProperty.call( context, steps[i] )  ){
                _c = context, context = context[steps[i]];
                if(i === steps.length -1) 
                    /**
                        To be able to restore things, we utilize a tuple literal
                        to maintain reference and other vital information.
                    */
                    return { at: steps[i], context: context, parent: _c };
            } else {
                /**
                    Will return on first occurence of an invalid path.
                */
                return undefined;
            }

        }

        // Implicit undefined is returned.
    }

    /**
        Get's the context as it was when stashed.
        returns undefined if no such context exists within the stash, or it was falsy
    */
    _retrieve(dotPath, context){
        context = context ? context : this.stash;
        return dotPath ? this._dotWalker(dotPath, context ) : context;
    }

    /**
        Returns a reference to the method in the Stash, you can't update the stash's
        reference here, but you can use it.
    */
    use(dotPath, context){
        return this._retrieve(dotPath, context).context._f || this._retrieve(dotPath, context).parent;
    }

    /**
        Will assign a stashed value to an object/context supplied.
    */
    attach(dotPath, to, as){
        var tmp = this._retrieve(dotPath, this.stash);
            to[ as || tmp.at ] = tmp.context._f; //TODO: confirm everything works.
            return to;
    }

}

API Examples

Construct a new Scalpel Stash object.;

    var s = new S5l();

Remove methods from an object/context, can accept an array of methods to be removed.

Expects an array of tuples { prop: object }, where prop will become the name used within the stash archive for the object/value.

When replaced with a value, the initial context will become a stub funtion which throws an error when called.

s.sever(['eval','alert'], [{window: window}]);
// window.alert && window.eval have been replaced with 
// stub functions.

Access the original method/prop, uses dotPath notation

window.alert('hi there');
// Error is thrown "alert has been disabled for security reasons."

s.use('window.alert')('Hi there.');
// calls the original alert function as it was initially

Attach a severed property/method to context, accepts a dotPaths as the first argument.

s.attach('window.alert', window);
// window.alert, now equals the original alert method.

s.attach('window.alert', window, 'scream');
// window.scream, now equals the original alert method.

Retrieve the stash, or parts of it.

s._retrieve(window.alert)
// stash.window.alert

or get the whole stash

s._retrieve()
// stash {...}

Release the stash, and reinitialize it.

s.purge();
// stash is now blank, {}

There is also a _dotToSteps and _dotWalker methods, these are not intended for public use and so are not documented here.

\$\endgroup\$
3
\$\begingroup\$

I will just comment on the security aspect. Let's say we want to disable window.eval:

let s = new S5l();
s.sever(['eval'], [{window: window}]);

Now, depending on your browser, you can still access eval either via

let frame = document.createElement('iframe');
document.body.appendChild(frame);
eval = frame.contentWindow.eval;

which might throw NS_ERROR_UNEXPECTED in Firefox but still works. In Chrome, one can alternatively restore overridden methods by accessing the DOMWindow prototype via

delete window.eval;
eval = window.constructor.prototype.eval;

Conclusion: There is probably no safe way to disable access to global attributes on the client side. Focus on preventing cross site scripting vulnerabilities etc. instead.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.