Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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.

like image 371
merv Avatar asked Oct 05 '12 14:10

merv


People also ask

How do I remove a script tag dynamically?

Dynamically removing an external JavaScript or CSS file To remove an external JavaScript or CSS file from a page, the key is to hunt them down first by traversing the DOM, then call DOM's removeChild() method to do the hit job.

Should I put script tags in head or body?

The script tag should always be used before the body close or at the bottom in HTML file. The Page will load with HTML and CSS and later JavaScript will load.

What happens if you place script elements in the head?

If it's in the HEAD section, the script will be parsed before any HTML or CSS elements are loaded. If your Javascript references any of the web page's elements, there may be a slight delay in the fancy effects you want to apply, or it may just not work at all.


2 Answers

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.

like image 123
Rob W Avatar answered Sep 30 '22 06:09

Rob W


Right, had another slightly less mad idea than my first, but it does depend on exactly what control you have on being able to insert tags in the head of the pages:

requirement

Put simply, if you can insert a <noscript> tag like I have below before any of the <script> declarations in the head, and you can then append a </noscript> tag to the end of the head, along with the final script snippet - you should be able to do whatever you want with the markup between the noscript tags before it is written back to the page.

The nice thing about this approach is that script-disabled agents will just ignore and parse the markup, but script-enabled agents will store the content up but not use it... exactly what is needed.

implementation

Whilst this is designed to be used with the head, it could easily be used the same way in the body, although it would have to be a separate implementation. This is because it has to work with a balanced and complete node tree, due to the nature of tags (unless you can manage to wrap the entire markup in noscript?!?).

Upsides/Downsides

It's not full-proof, because scripts can lie outside of the head and body tags - at least before they are parsed - but it seems to work pretty confidently on everything I've tested so far... and it doesn't rely on a smattering of randomly ajax-powered code that'll break at the first sign of a browser update ;)

Plus I also like the idea of script tags within noscript tags...

<head> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <noscript id="__disabled__">   <script src="jquery.js"></script>   <title>Another example</title>   <script>alert(1);</script>   <link rel="stylesheet" type="text/css" href="core.css" />   <style>body { background: #ddd; }</style> </noscript> <script> (function(){   var noscript = document.getElementById('__disabled__');   if ( noscript ) {     document.write(       String(noscript.innerHTML)         /// IE entity encodes noscript content, so reverse         .replace(/&gt;/gi,'>')         .replace(/&lt;/gi,'<')         /// simple disable script regexp         .replace(/<script[^>]*>/gi,'<'+'!--')         .replace(/<\/script>/gi,'//--'+'>')     );   } })() </script> </head> 
like image 27
Pebbl Avatar answered Sep 30 '22 08:09

Pebbl