Client-Side Dynamic Removal of <script> Tags in <head>

merv picture merv · Oct 5, 2012 · Viewed 8.5k times · Source

Is it possible to remove script tags in the <head> of an HTML document client-side and prior to execution of those tags?

On the server-side I am able to insert a <script> above all other <script> tags in the <head>, except one, and I would like to be able to remove all subsequent scripts. I do not have the ability to remove <script> tags from the server side.

What I've tried:

(function (c,h) {
  var i, s = h.getElementsByTagName('script');
  c.log("Num scripts: " + s.length);
  i = s.length - 1;
  while(i > 1) {
    h.removeChild(s[i]);
    i -= 1;
  }
})(console, document.head);

However, the logged number of scripts comes out to only 1, since (as @ryan pointed out) the code is being executed prior to the DOM being ready. Although wrapping the code above in a document.ready event callback does enable proper calculation of the number of <script> tags in the <head>, waiting until the DOM is ready fails to prevent the scripts from loading.

Is there a reliable means of manipulating the HTML prior to the DOM being ready?

Background

If you want more context, this is part of an attempt to consolidate scripts where no option for server-side aggregation is available. Many of the JS libraries being loaded are from a CMS with limited configuration options. The content is mostly static, so there is very little concern about manually aggregating the JavaScript and serving it from a different location. Any suggestions for alternative applicable aggregation techniques would also be welcome.

Answer

Rob W picture Rob W · Oct 10, 2012

Since you cannot prevent future <script> tags from evaluating (whenever the </script> tag has been found, the corresponding code of <script> is fetched and evaluated. <script src> will block a document from loading further till the source is fetched unless the async attribute is set), a different approach need to be taken.
Before I present the solution, I ask: What can prevent a script within a <script> tag from executing? Indeed,

  1. Removal of <script> from the source code.
  2. Adding a Content Security policy directive to block scripts from certain sources.
  3. Triggering a (runtime) error.

1 is obvious, and 2 can be derived from the documentation, so I'll focus on 3. The examples below are obvious, and need to be adjusted for real-world use cases.

Proxying

Here's a general pattern for proxying existing methods:

(function(Math) {
   var original_method = Math.random;
   Math.random = function() {
       // use arguments.callee to read source code of caller function
       if (/somepattern/.test(arguments.callee.caller)) {
           Math.random = original_method; // Restore (run once)
           throw 'Prevented execution!';
       }
       return random.apply(this, arguments); // Generic method proxy
   };
})(Math);
// Demo:
function ok()    { return Math.random(); }
function notok() { var somepattern; return Math.random(); }

In this example, the code-blocker runs only once. You can remove the restoration line, or add var counter=0; and if(++counter > 1337) to restore the method after 1337 calls.

arguments.callee.caller is null if the caller is not a function (eg. top-level code). Not a disaster, you can read from the arguments or the this keyword, or any other environment variable to determine whether the execution must be stopped.
Demo: http://jsfiddle.net/qFnMX/

Deny setters / getters

Here's a general pattern for breaking setters:

Object.defineProperty(window, 'undefinable', {set:function(){}});
/*fail*/ function undefinable() {} // or window.undefinable = function(){};

Demo: http://jsfiddle.net/qFnMX/2/

And getters, of course:

(function() {
    var actualValue;
    Object.defineProperty(window, 'unreadable', {
        set: function(value) {
            // Allow all setters for example
            actualValue = value;
        },
        get: function() {
            if (/somepattern/.test(arguments.callee.caller)) {
                // Restore, by deleting the property, then assigning value:
                delete window.unreadable;
                window.unreadable = actualValue;
                throw 'Prevented execution!';
            }
            return actualValue;
        },
        configurable: true // Allow re-definition of property descriptor
    });
})();
function notok() {var somepattern = window.unreadable; }
// Now OK, because 
function nowok() {var somepattern = window.unreadable; }
function ok()    {return unreadable;}

Demo: http://jsfiddle.net/qFnMX/4/

And so on. Look in the source code of the scripts you want to block, and you should be able to create a script-specific (or even generic) script-breaking pattern.

The only downside of the error-triggering method is that the error is logged in the console. For normal users, this should not be a problem at all.