Skip to main content
menu

BMT Systems

  WEB CODING, TUTORIALS, AND COOKBOOK  

Mobile Navigation Controls

Deferring JavaScript and CSP 3 strict-dynamic

Thomas M. Brodhead
  • Defer all JavaScript until after the DOMContentLoaded or window onload event.
  • Implement a deferral routine for the development cycle.
  • Implement a deferral routine for deployment/production.
  • Use CSP 3 strict-dynamic to prevent JavaScript from being compromised by an attacker.
  • Bust any browser cache of a previous version of the JavaScript for returning site visitors.

Get JavaScript out of your <head>

JavaScript is render-blocking. The browser halts HTML parsing when it encounters JavaScript, instead reading and executing the JavaScript before resuming to parse the HTML. Because JavaScript typically operates on DOM elements and interacts with their CSSOM properties, it makes little sense to execute JavaScript before the DOM and CSSOM are constructed. Therefore, unless the JavaScript expedites the initial navigation of the critical rendering path (e.g., preloading fonts or other critical resources, etc.), it should be deferred until after the <head> is executed and the <body> is fully parsed, loaded, and ready.

The take-away: Except in special circumstances, always put JavaScript at the end of the <body> element.

Don’t depend on the async or defer <script> attributes

The async <script> attribute provides immediate parallel downloading of scripts. It does not guarantee in what order they will be executed, nor a way to indicate when they should be executed, as execution of each script is immediate upon download.

The defer <script> attribute guarantees that scripts will be executed in the order they occur in the HTML, and it moves their execution to a point in between the domInteractive state and the DOMContentLoaded event. There is no way to prescribe a different execution point for a defer <script>, such as after the DOMContentLoaded event or after the window onload event. A serious concern is that the download process of a defer script is render-blocking.

Here’s a quick review of the page construction mileposts just mentioned:

  • domInteractive: The HTML is parsed and the DOM is constructed.
  • DOMContentLoaded: This follows domInteractive, and occurs after the CSS is parsed and the CSSOM is constructed.
  • onload: This follows domInteractive and DOMContentLoaded, and occurs after all document resources (e.g., images) have finished loading.

A more complete picture of DOM and window loading events is provided here1:

DOM Loading Event Diagram

Depending on your JavaScript libraries and original routines, you may need a specific execution point for your scripts. If any script assumes document resources are available, it should be deferred until after the window onload event. But if any script has a window onload event, it should be loaded earlier, perhaps after DOMContentLoaded, or else it won’t execute. A JavaScript deferral technique with granular control is desirable.

A single JavaScript resource file for all

To ensure all scripts are loaded in order and to minimize resources fetching, the best practice is to concatenate all scripts into a single file. This requires only one <script> resource for retrieval during page rendering.

For site deployment, a minified version of each JavaScript library should be used, and any original JavaScript routines of your own should be minified with the Google Closure Compiler or a similar minifier, with all scripts combined in one file. But during development, it’s a different story: different libraries may need to be tested, and your original routines may need modification as they and the site are developed.

The scripts—minified for production or human-readable for development—should be loaded without disrupting the browser’s parsing of the rest of the HTML, and the scripts should be executed after the desired page construction event (e.g., DOMContentLoaded or window onload), otherwise the results of the scripts may differ between development and deployment.

A simple technique may be used both for development and deployment, with a subtle coding difference between the two. We’ll start with the deployment technique and work backwards to the development technique.

Deployment JavaScript loading: Basic form

Below is the loading script for deployment, the only script element needed. It loads a single JavaScript file, the concatenation of all minified JavaScript for the page, here named defer.min.js (any name or directory location will do). There will be two modifications to this routine discussed later, so don’t copy this code yet. Even so, the basic form presented here should be understood, here using the DOMContentLoaded event as the trigger:

brightness_high content_paste
(function () {

    'use strict';

    function deferJS() {
        var script = document.createElement('script');
        script.src = 'defer.min.js';
        script.crossOrigin = 'anonymous';
        document.body.appendChild(script);
        script.parentNode.removeChild(script);
// remove this script:
        script = document.currentScript || document.scripts[document.scripts.length - 1];
        script.parentNode.removeChild(script);
    }

    if (window.addEventListener) {
        window.addEventListener('DOMContentLoaded', deferJS, false);
    } else if (window.attachEvent) {
        window.attachEvent('onreadystatechange', deferJS);
    } else {
        window.onload = deferJS;
    }

}());

If you wanted to use the window onload event as the trigger, the event listener would be:

brightness_high content_paste
    if (window.addEventListener) {
        window.addEventListener('load', deferJS, false);
    } else if (window.attachEvent) {
        window.attachEvent('onload', deferJS);
    } else {
        window.onload = deferJS;
    }

...but so many JavaScript libraries make use of the window onload event for triggering actions that it is probably safer to use the DOMContentLoaded event as the trigger. The window onload event code above is therefore provided for completeness.

But what does the code do? The function deferJS creates an original <script> element, links it to defer.min.js, then appends it to the DOM (causing defer.min.js to execute), and finally removes the <script> element from the DOM. That function is invoked by an event listener selected in a conditional block, and there are two forms of that block, one targeting the DOMContentLoaded event, the other targeting the load event.

In the first form (which is the form used for the remainder of this tutorial), if addEventListener is supported, the window DOMContentLoaded event is used as the trigger, and the deferJS function executes when it the event is detected. Alternatively, if addEventListener is not supported but attachEvent is supported, the code uses the onreadystatechange event as the trigger for executing deferJS. If all else fails, the window onload event is employed. In the alternative version to this, the window load or onload event is used as the trigger using the same flow of conditional logic.

Again, though, there are two lines of code purposely left out of this routine for now. We’ll explain their utility and add them in later, so don’t copy this code yet. Next we must take a tour through site security policies for JavaScript.

Site security, CSP, and JavaScript

If you do not prevent a malicious attacker from tampering with your JavaScript or injecting rogue scripts, your site visitors are at risk. Setting a CSP (Content Security Policy) is the best line of defense. Sent as a header from the server, a good CSP limits what may be executed in your site’s code.

In CSP level 2, the script-src directive prevents all inline scripts from executing (except those with nonces, discussed later) and allows only scripts with whitelisted SRCs to execute. For example:

brightness_high content_paste
script-src 
    'self'
    https://fonts.googleapis.com/
    https://code.jquery.com/jquery-3.2.1.min.js;

This policy would allow scripts from the same domain, fonts.googleapis.com, and the specific script jquery-3.2.1.min.js at code.jquery.com to execute.

But there’s a problem here. If any of the scripts generate new scripts or execute eval(), those descendant routines will fail to execute because they are not whitelisted. The directive unsafe-eval is required for this, which isn’t much protection:

brightness_high content_paste
script-src
    'self'
    'unsafe-eval'
    https://fonts.googleapis.com/
    https://code.jquery.com/jquery-3.2.1.min.js;

Also, if an attacker compromised any of the named scripts in the CSP, then they would be loaded into your site with no questions. Finally, no amount of whitelisting can account for the trivial ways that the policy can be circumvented. Google wrote extensively on this problem and detailed its findings in CSP Is Dead, Long Live CSP! On the Insecurity of Whitelists and the Future of Content Security Policy, which concludes with the CSP level 3 solution.

CSP level 3: strict-dynamic to the rescue

The CSP level 3 solution to this problem is the strict-dynamic directive. It allows scripts and their descendant processes to execute when they are identified in the header by (1) a nonce or (2) a cryptographic hash of the script itself.

With strict-dynamic, you are instructing the browser to trust scripts and their descendant processes when you have marked the scripts as safe. This makes hosting JavaScript libraries on your own domain (or, as we are doing, including them in full in a single script hosted on your site) paramount to security, as you can ensure the integrity of the source script.

Nonces are appropriate for dynamically generated HTML. Script hashes are the only option for static HTML. We’ll cover nonces first.

Security by nonces

Nonce is short for “Number used ONCE,” and should be a pseudo-random number subjected to a SHA-256 hashing, e.g. qR+MkZw0/j/V7lHh193p7rw=. (The hashing merely provides obfuscation to prevent predictability by an attacker.) The nonce must be generated anew every time the site is accessed, which is why nonces are perfect when serving dynamic HTML but not tenable when serving static HTML.

When generating the HTML, create a nonce for the CSP header and for the <script> elements in the HTML. Provided a random nonce qR+MkZw0/j/V7lHh193p7rw=, within the CSP strict-dynamic header it would appear this way:

brightness_high content_paste
Header set Content-Security-Policy
    script-src 'strict-dynamic' 'nonce-qR+MkZw0/j/V7lHh193p7rw=';

And within each SCRIPT element of your HTML:

brightness_high content_paste
<script nonce=qR+MkZw0/j/V7lHh193p7rw=>
    console.log('This site is safe!');
</script>

Security by script hash

The alternative to nonces—and the only viable strict-dynamic solution for sites serving static HTML—is a SHA hash of the script itself. With this method, you determine and employ either the SHA-256, SHA-384, or SHA-512 of the entire script. The hash does not appear in the HTML but is issued in the CSP header only.

The SHA-256 hash of the loader script in the Deployment JavaScript loading: Basic form section above is SibLFHM1FCZIO1eGPCccC0+vJikJpK8saLLSArRu+2U=. This CSP issues it with a strict-dynamic directive:

brightness_high content_paste
Header set Content-Security-Policy
    script-src 'strict-dynamic' 'sha256-SibLFHM1FCZIO1eGPCccC0+vJikJpK8saLLSArRu+2U=';

The <script> element itself requires no nonce attribute; the browser will compute the SHA of the script, compare it with the SHA in the policy, and execute the script only if the SHAs match. Any number of SHAs may be listed, each corresponding to a separate <script> element in the HTML. However, with our method, only one <script> element is needed, so only one SHA is required.

NB: Don't employ the SHA hash provided above on a block-copy of the script above; recalculate it provided your code formatting. If the code follows line-breaks within the <script> tag, then the line-break(s) will need to be part of the SHA hash.

CSP for browsers without CSP level 3 support

This policy, appropriately edited, should serve older browsers without CSP 3 support:

brightness_high content_paste
Header set Content-Security-Policy
    script-src 'strict-dynamic' 'unsafe-inline' ['unsafe-eval'] https: http: ['sha256-{random}' | 'nonce-{random}'];

This means that when strict-dynamic is not available:

  • https: and http: confer trust to those protocols
  • unsafe-inline confers trust to inline scripts (<script> elements).
  • unsafe-eval allows execution of eval() in the absence of strict-dynamic.

See Google’s article Content Security Policy: Strict CSP for a complete discussion.

SRI to secure the deferred script

Regardless of whether you’re using nonces or a script hash, there’s one last security problem for our deferJS script at this stage: the SRC script itself within the deferJS function isn’t secure. If an attacker compromised defer.min.js, then our security measures so far would fail, as we've instructed the browser to trust all descendant processes of the <script>. That would mean the compromised defer.min.js script would be executed, and our site would be breached. To solve this, force a Sub-Resource Integrity (SRI) check on defer.min.js.

First calculate the SHA of defer.min.js itself (not the deferJS <script> element). Then add that SHA as an integrity attribute of the defer.min.js <script> element created by the deferral routine:

brightness_high content_paste
(function () {

    'use strict';

    function deferJS() {
        var script = document.createElement('script');
        script.src = 'defer.min.js';
        script.crossOrigin = 'anonymous';
        script.integrity = 'sha256-pL+VBoR3r4z7O6cNLVKu/K4/GfpYmSyDivzzk4qB72E=';
        document.body.appendChild(script);
        script.parentNode.removeChild(script);
// remove this script:
        script = document.currentScript || document.scripts[document.scripts.length - 1];
        script.parentNode.removeChild(script);
    }

    if (window.addEventListener) {
        window.addEventListener('DOMContentLoaded', deferJS, false);
    } else if (window.attachEvent) {
        window.attachEvent('onreadystatechange', deferJS);
    } else {
        window.onload = deferJS;
    }

}());

Doing this ensures that your script is the one loaded and executed, not a replacement script by an attacker. And again, this is for the deployment version of the deferJS script, not the development version, which we’ll cover in a bit.

But there’s one last thing to implement: cache-busting.

Cache-bust your deferred script

A visitor’s browser may cache the defer.min.js script, using the cached version on repeat visits, thus preventing your own revisions to your JavaScript from being employed. Using a time-stamp query string to thwart this is not advised2. A better solution is autoVersioning: retrieving the time stamp of the file and rewriting the file name to include the time stamp before the extension, like this:

brightness_high content_paste
defer.min.20170912122907.js

A server-side routine is needed to generate this. A sample PHP routine for this task would be:

brightness_high content_paste
function autoVersion($url) {

// Set $external_site = TRUE if the resource is not on the localhost or parent domain;
// write original code for that HERE.

// If we're retrieving static content from an external site:
    if ($external_site) {
        $headers = get_headers($url, 1);
        if ($headers && (strpos($headers[0], '200') !== FALSE)) {
            $time = strtotime($headers['Last-Modified']);
            $date = date("YmdHis", $time);
        } else {
            $date = '19990221125549';
        }
// Otherwise, we're retrieving it from the dynamic server or from the local server:
    } else {
        $date = date("YmdHis", filemtime($url));
    }

// Add date & time stamp to resource name: https://www.example.com/js/defer.min.20160221212802.js
    $name = explode('.', $url);
    $extension = array_pop($name);
    array_push($name, $date, $extension);
    $fullname = implode('.', $name);

    return $fullname;

}

This covers what to do for your HTML, whether generated dynamically or statically. The next step is to instruct your server to use a RegEx to strip out the time stamp from qualifying files. An example rewrite rule for an Apache server configuration or .htaccess file would be:

brightness_high content_paste
# Strip out numeric time stamp from CSS, JS, JSON, and ICO files:
RewriteEngine on
RewriteRule ^((ftp|http|https):\/\/)?(.*)\.([\d]+)?\.(css|js|json|ico)$ $1$3.$5 [L] # Strip out the time stamp

This will allow the deferJS script to fetch the most recent version of defer.min.js.

Loading JavaScript on the deployment/production site

Putting it all together, the deferJS script for your deployment site would be similar to:

brightness_high content_paste
(function () {

    'use strict';

    function deferJS() {
        var script = document.createElement('script');
        script.src = 'defer.min.20170912122907.js';
        script.crossOrigin = 'anonymous';
        script.integrity = 'sha256-pL+VBoR3r4z7O6cNLVKu/K4/GfpYmSyDivzzk4qB72E=';
        document.body.appendChild(script);
        script.parentNode.removeChild(script);
// remove this script:
        script = document.currentScript || document.scripts[document.scripts.length - 1];
        script.parentNode.removeChild(script);
    }

    if (window.addEventListener) {
        window.addEventListener('DOMContentLoaded', deferJS, false);
    } else if (window.attachEvent) {
        window.attachEvent('onreadystatechange', deferJS);
    } else {
        window.onload = deferJS;
    }

}());

Either use a nonce attribute in the <script> element or issue a header with a SHA of the script itself (with any initial or final line-breaks) to secure the routine and enable execution.

Loading JavaScript on the development site

During development, we’ll need to enable switching scripts, revising scripts, and trying out unanticipated libraries. We’ll use the deferJS <script> to call not defer.min.js but a utility script, loader.js, whose code is:

brightness_high content_paste
(function () {

    'use strict';

    var buildScriptArray;
    var loadScriptsSynchronously;
    var scriptArray;

    buildScriptArray = function (script) {
        scriptArray.push(script);
    };

    loadScriptsSynchronously = function () {
        var script = document.createElement('script');
        script.crossOrigin = 'anonymous';
// return and remove the first script in the script array:
        script.src = scriptArray.splice(0, 1);
// if there are subsequent scripts required for loading, call this function again:
        if (scriptArray.length) {
            script.onload = loadScriptsSynchronously;
        }
        document.body.appendChild(script);
        script.parentNode.removeChild(script);
    };

    scriptArray = [];

    buildScriptArray('javaScriptLibrary1.js');
    buildScriptArray('javaScriptLibrary2.js');
    buildScriptArray('javaScriptLibrary3.js');
// etc.

    loadScriptsSynchronously();

}());

Here we load JavaScript libraries using a loadScriptsSynchronously function in loader.js. It appends any number of <script> elements to the DOM in the same manner as the deferJS <script> that calls it from the HTML, but with a twist. The important difference between the two routines is that loader.js ensures that the JavaScript libraries are loaded synchronously. The reason for this may be crucial, depending on which libraries you are employing and how your own code (stored in your own library file) is constructed.

Because JavaScript is render-blocking, JavaScript libraries loaded through script elements with src attributes are called and executed synchronously. Likewise, the deployment/production deferJS routine ensures that libraries are loaded in sequence by virtue of their sequential ordering in the defer.min.js file. To ensure the JavaScript libraries are loaded and executed synchronously during development (during which time you may be experimenting with different libraries, adding and subtracting to them and altering their ordering as you develop the site code), loader.js here employs the onload attribute on each script element to specify the next library in the scriptArray in sequence. This ensures that each script is loaded fully before the next one is appended to the DOM. This in turn prevents unpredictable asynchronous loading of libraries whose load-order needs to be preserved.

Notice that there are no CSP attributes on the created <script> elements, nor any autoVersioning: we don't need those things here. Script security via strict-dynamic is applied in the main HTML on the calling script, the deferJS <script>, modified during development to call loader.js instead of defer.min.js; trust is conferred for it and its descendant processes via strict-dynamic with either a nonce or a SHA hash:

brightness_high content_paste
(function () {

    'use strict';

    function deferJS() {
        var script = document.createElement('script');
        script.src = 'loader.js';
        script.crossOrigin = 'anonymous';
        document.body.appendChild(script);
        script.parentNode.removeChild(script);
// remove this script:
        script = document.currentScript || document.scripts[document.scripts.length - 1];
        script.parentNode.removeChild(script);
    }

    if (window.addEventListener) {
        window.addEventListener('DOMContentLoaded', deferJS, false);
    } else if (window.attachEvent) {
        window.attachEvent('onreadystatechange', deferJS);
    } else {
        window.onload = deferJS;
    }

}());

Again, this is the same as the first form of the deferJS <script>, provided in Deployment JavaScript loading: Basic form above. The only difference is that it calls loader.js, not defer.min.js.

For site development, this version of the deferJS <script> should be employed in the HTML. Specific JavaScript libraries should be loaded with the loader.js, which will handle all new scripts and changes to old scripts. The strict-dynamic CSP applied to the deferJS <script> in the HTML allows anything in loader.js to execute, so we may load arbitrary JavaScript libraries with it; each internal loading will be allowed because it is a descendant routine of the deferJS <script>.

Using loader.js To Employ Externally Hosted JS Libraries with SRIs

If we wished to load externally hosted scripts and include Sub-Resource Integrity (SRI) verification on them, a simple modification of loader.js as follows would suffice:

brightness_high content_paste
(function () {
    'use strict';
    var buildScriptArray;
    var integrityArray;
    var loadScriptsSynchronously;
    var scriptArray;

// integrity (SRI) is optional 2nd parameter
    buildScriptArray = function (script, integrity) {
        scriptArray.push(script);
        integrityArray.push(integrity);
    };

    loadScriptsSynchronously = function () {
        var integrity;
        var script;
        script = document.createElement('script');
// return and remove the first script in the script array:
        script.src = scriptArray.splice(0, 1);
        script.crossOrigin = 'anonymous';
// Check for and retrieve any SRI
        integrity = integrityArray.splice(0, 1);
        if (integrity[0]) {
            script.integrity = integrity;
        }
// if there are subsequent scripts required for loading, call this function again:
        if (scriptArray.length) {
            script.onload = loadScriptsSynchronously;
        }
        document.body.appendChild(script);
        script.parentNode.removeChild(script);
    };

    scriptArray = [];
    integrityArray = [];

// SRI may be included optionally as a second parameter:
    buildScriptArray('https://code.jquery.com/jquery-3.2.1.slim.min.js', 'sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN');
    buildScriptArray('https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js', 'sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q');
    buildScriptArray('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js', 'sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl');

    loadScriptsSynchronously();

}());

Here, we construct an integrityArray object that holds any SRI attributes we wish to include as attributes of the scripts. The second parameter in buildScriptArray is optional, so you may alternate between scripts with and without integrity attributes.

Conclusion

We've created 3 routines:

  • A deferJS routine for the deployment/production site.
  • A loaderJS routine for the development site.
  • A loader.js utility routine for the devlopment site.

Coupled with a solid CSP 3 strict-dynamic header and appropriate nonces or hashes—and along with an SRI and a cache-busting autoVersion for the defer.min.js file—your JavaScript will be loaded with granular control and with security.

Notes

1 See this diagram from W3 for an exhaustive listing of browser rendering events.

2 See What Is Fingerprinting and Why Should I Care? and also Revving Filenames: Don’t Use Querystring

Aside