Avoiding pollution of globals via iframe script lo

2019-02-02 17:16发布

问题:

Problem...

Poorly-coded scripts exist which need to be included on a web page.

These scripts pollute the global scope by doing things like:

  • Assigning values to undeclared identifiers
  • Adding properties to built-in constructor functions (like Object and Array) and their prototypes
  • Other nasty stuff.

Solution?

I want to include the scripts without the adverse side effects. I think it can be achieved by loading the script in an iframe and exporting objects as properties of the parent window. Here's what Ive got so far:

<script>

(function(){
  var g=this, frameIndex=frames.length, f=document.createElement('iframe');

  // hide it like this instead of display:none, because some old browser ignores
  // iframes with display:none, or is this an ancient habit I can drop?
  f.style.width='0px'; f.style.height='0px'; 
  f.style.border='none'; f.style.position='absolute';

  // append it to document.body or document.documentElement?
  // documentElement seems to work before body is loaded,
  // but is it cross-browser safe?  
  document.body.appendChild(f);

  // window object for our iframe
  var w=frames[frameIndex];

  // callback function pulls the object into the current window when script loads
  w.cb=function(){ g.SomeObject=w.SomeObject };

  // will this work on IE, or do I need to use document.createElement?
  // wanted to avoid document.createElement in this case because I'm not sure 
  // whether to call it from window.document or frames[frameIndex].document
  w.document.innerHTML='<script onload="cb()" src="myscript.js"><\/script>';

}());

</script>

Questions:

  • Will there be potential havoc if a script modifies built-in prototypes and I move it into another window, or will my parent window's built-ins stay clean and everything will "just work?"

  • Is this idea going to work on 'most' browsers, or is there a show-stopper? Haven't tested on anything besides chrome and moz so far.

  • I'd like to remove the iframe after pulling the object into the current window, but moz will lose the object reference if the iframe is removed. Does anyone know of a way around that?

  • Has this already been done, or is there a better way to accomplish my goal? If so, what's the name of the script or technique I should to be looking for?

(question transplanted from here)

回答1:

To copy a function you could cast it to a string and then eval it.... The code below also demonstrates that the iframe can be removed after doing this and your copy remains intact.

The following code sample using FF

Child.html snippet

<script>

//
// modify the prototype
//
Object.prototype.test = function(msg)
{
        alert(msg);
};  

//
// Simply declare a function
//
var whoo_hoo = function(){alert("whoo hoo");}
</script>

Parent with iframe:

 <iframe id="help_frame" src="http://localhost/child.html"
 onLoad="javascript:Help.import_functions(this)"></iframe>

    <script>
    var Help = {

          imported_function :null,
              import_functions : function(iframe)
   {
    this.imported_function = String(iframe.contentWindow.whoo_hoo);
    eval("this.imported_function = " + this.imported_function);
    iframe.parentNode.removeChild(iframe);

   //
   // displays 'whoo hoo' in an alert box
   //
   this.imported_function();

   try
   {
      //
      // If the Object prototype was changed in the parent
      // this would have displayed 'should not work' in an alert
      //
      this.test('should not work');
   }
   catch(e){alert('object prototype is unmodified');}

   }, 
    </script>

http://thecodeabode.blogspot.com/



回答2:

code for comment under Gabriel's answer..

var r = {
    init : null,
    _init: function(){
        var obj = new XMLHttpRequest();
        obj.onreadystatechange = function(){
            if ((this.status == 200) && this.readyState==4){
                try{
                    eval("r.init = function(){" + this.responseText + "}");
                    r.init();
                } catch(e){/*something bad in the script...*/}
            }
        }
        obj.open("GET","/jspolute_bad.js", true);
        obj.send();
    }   
}
r._init();

With methods being added to prototype, you might be in trouble if one or two of the exported functions expect the method as it is modified in foreign code. tedious solution that comes to mind is to Regex the responseText before eval'ing it for array.prototype,string.prototype and fix it some how. Will try that and let you know.. but it would mostly cater to straightforward scripts only.



回答3:

This could be a possible solution:

  • wrap all the foreign code into a class
  • make all the undeclared identifiers members of that class
  • before invoking the messy code, make a copy of the built-in classes and name them differently
    (is this possible??).

I think this should solve all the problems.

With my suggestion, your sample

var badA = "hahaha";
this.badB = "hehehe";
badC = "hohoho";

String.prototype.star = function(){ return '***' + this + '***' }

var somethingUseful = {
  doStuff: function () {
    alert((badA + badB + badC).star());
  }
}

should get like this

// Identifies the added properties to prototypes (ie String and Function)
// for later filtering if you need a for-in loop.
var stringAddons = [];
var functionAddons = []
var _string = new String();
var _function = function() {};
for (var property in _string) { if (!_string.hasOwnProperty(property)) { stringAddons.push(property); }}
for (var property in _function) { if (!_function.hasOwnProperty(property)) { functionAddons.push(property); }}

// Wraps the undeclared identifiers
var global = function()
{
  this.badA = "hahaha";
  this.badB = "hehehe";
  this.badC = "hohoho";

  String.prototype.star = function(){ return '***' + this + '***' }

  this.somethingUseful = {
    doStuff: function () {
      alert((global.badA + global.badB + global.badC).star());
    }
  }
}
var global = new Global();
global.somethingUseful.doStuff();

The tricky part is making ALL the undeclared identifiers global properties. Maybe a good regex script could make it. Im not that good with regex :)



回答4:

None of the answers so far seem to work as well as the iframe thing. I'm fairly convinced the iframe jail is going to be the solution to the problem. I'm putting my current solution here as an answer because it seems to work better than the other answers provided so far. I'd really like to refine this iframe technique into something foolproof, and I'd love to understand how the modified prototypes work. This example works in chrome and moz.

test.js

var badA = "hahaha";
this.badB = "hehehe";
badC = "hohoho";

String.prototype.star = function(){ return '***' + this + '***' }

var somethingUseful = {
  doStuff: function () {
    alert((badA + badB + badC).star());
  }

}

test.html

<html>
  <head>
    <script>
    /** 
      safeLoad - load a script in an iframe jail

      @param {String} scriptPath    path to a javascript file
      @param {String} target        name of global object to import
      @param {Function} callback    function to execute after script loads
    */ 
    function safeLoad (scriptPath, target, callback) {
      var g=this, f=document.createElement('iframe'), frameIndex=frames.length;

      f.style.width='0px'; 
      f.style.height='0px'; 
      f.style.border='none'; 
      f.style.position='absolute';

      f.onload=function(){
        var w=frames[frameIndex];
        var s=w.document.createElement('script');
        s.src=scriptPath;
        s.onload=function(){
          g[target]=w[target];
          if (callback && callback.apply) callback(w);
        };
        w.document.body.appendChild(s);
      }
      document.documentElement.appendChild(f);
    }
    </script>  

    <script>

    safeLoad('test.js', 'somethingUseful', function init () {
      // next line should give ***hahahahehehehohoho***
      somethingUseful.doStuff();
      // next line should give undefinedundefinedundefinedundefined
      alert(typeof badA + typeof badB + typeof badC + String.prototype.star);
    });

    </script>   
  </head>
</html>