jspm

JavaScript Integrity Manifests with Import Maps

Guy Bedford, August 5th 2024

With the new "integrity" field for import maps, recently released in Chrome 127 thanks to work from Shopify engineering, we now have a new security primitive for JS modules on the web with the ability to define a module integrity manifest.

Before this, there were only two ways to define Subresource Integrity for JavaScript code on the web:

  1. With the integrity directly on the script tag: <script type="module" integrity="sha386-..." src="...">.
  2. With the integrity directly on a preload tag: <link rel="modulepreload" integrity="sha386-..." href="...">.

These requirements for integrity have limitations in that:

The above often make it inhibitatively difficult to ship ES modules on the web with full integrity for JS resources, or to even consider a Content-Security-Policy: require-sri-for script; integrity policy when shipping ES modules.

The Import Map "integrity" field

The import map "integrity" field in import maps allows populating a mapping of URLs to their integrity attributes.

These integrity values are then applied for all JS imports matching the defined URLs, applying both to modules loaded statically on page initialization along with their dependencies, and those lazily loaded after the initial page load.

Having all the module integrity in this new single manifest makes it much easier to verify and update integrity across an application, as well as to support require-sri-for.

Example

To take advantage of this new field when using JSPM, the JSPM Generator and CLI now support a new integrity option and flag.

For example, consider the following HTML application app.html:

<!doctype html>
<script type="importmap">
{
  "imports": {
    "lit": "https://cdn.jsdelivr.net/gh/lit/dist@3.1.4/core/lit-core.min.js"
  }
}
</script>
<script type="module" src="./app.js"></script>
<body>
  <simple-greeting name="World"></simple-greeting>
</body>

where app.js contains:

import {html, css, LitElement} from 'lit';

class SimpleGreeting extends LitElement {
  static styles = css`p { color: blue }`;
  static properties = {
    name: {type: String}
  };
  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}
customElements.define('simple-greeting', SimpleGreeting);

We can use the JSPM CLI to update this HTML page to relink the modules outputting the new import map with integrity back into the same HTML page:

jspm link app.html --integrity -o app.html

This will update app.html to now contain:

<!doctype html>
<script async src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js" crossorigin="anonymous" integrity="sha384-ie1x72Xck445i0j4SlNJ5W5iGeL3Dpa0zD48MZopgWsjNB/lt60SuG1iduZGNnJn"></script>
<script type="importmap">
{
  "imports": {
    "lit": "https://cdn.jsdelivr.net/gh/lit/dist@3.1.4/core/lit-core.min.js"
  },
  "integrity": {
    "./app.js": "sha384-JBeRlySsOPUakm9Jdnn7Kcmbf/FFAGhbcEcwJkBXYCdtAtG1oVv5/PVycS1nsNKC",
    "https://cdn.jsdelivr.net/gh/lit/dist@3.1.4/core/lit-core.min.js": "sha384-1XCsIc9Rfy/YoXO1AeA7koK9Donixq1VQYObT7umyw25v2v8dBBumjdE8cgOg4aW"
  }
}
</script>
<script type="module" src="./app.js"></script>
<body>
  <simple-greeting name="World"></simple-greeting>
</body>

With the above "integrity" configuration, we don't need to consider any further integrity attributes at all for a top-level <script type="module" src=""> site, instead the integrity will always be verified for all of the modules imported.

If the network returns a different source for a given JS file, then a network error is thrown, instead of there being a potential vulnerability.

Try it out on the online generator here turning on the Integrity toggle at the top of the page to see an example of the generated import map integrity field.

ES Module Shims Support

The ES Module Shims modules polyfills project now fully includes a polyfill for import map "integrity".

Whenever the polyfill is engaging (either in shim mode, or when statically unsupported modules features are used), "integrity" metadata will be passed to the underlying fetch request used by the polyfill, to ensure that even when the polyfill is engaging integrity is still supported.

While not a comprehensive security model (unless using shim mode), the ability to support "integrity" when the polyfill is engaging still expands the coverage of the integrity checks for users of an application, still raising the overall level of security.

A Future with Integrity

Integrity should really be the default for all JS applications, especially those relying on third-party CDNs!

This can prevent the fallout of attacks like the recent polyfill.io supply chain attack, where users relying on the polyfill.io CDN to serve JS code found that code being tampered with to create targeted redirects across all sites embedding these JS polyfills.

To get there, integrity should become a first-class part of our JS deployment workflows, which this new "integrity" field can help enable.